מפתח

ברוכים הבאים!

קורס מקיף שילווה אתכם ב-React מהצעד הראשון ועד רמה מתקדמת. הוא בנוי כמסלול רציף שמתחיל בעקרונות היסוד וממשיך עד לנושאים המתקדמים ביותר - ניהול מצב עם Redux, בדיקות (Testing) ופיתוח מובייל עם React Native.

למה React?

  • 🌍 הספרייה הנפוצה בעולם לבניית ממשקי משתמש
  • 🔄 Virtual DOM שמאפשר עדכוני מסך מהירים ויעילים
  • 📦 קומפוננטות מודולריות שאפשר לעשות בהן שימוש חוזר
  • 💪 מערכת כלים עשירה וקהילת מפתחים עצומה

📚 מבנה הקורס

חלק א' - יסודות

מספר נושא קובץ
1 רקע והיסטוריה 01_background.md
2 התקנה והגדרות 02_installation.md
3 תחביר JSX 03_jsx_syntax.md
4 קומפוננטות 04_components.md
5 Props 05_props.md
6 State 06_state.md
7 אירועים 07_events.md
8 Hooks בסיסיים 08_hooks.md
9 רינדור מותנה 09_conditional.md
10 רשימות 10_lists.md
11 תרגילים 11_exercises.md

חלק ב' - Lifecycle & Patterns

מספר נושא קובץ
12 Lifecycle & useEffect 12_lifecycle.md
13 Props Drilling & Lifting State 13_props_drilling.md
14 Component Communication 14_component_patterns.md

חלק ג' - Context & Hooks מתקדמים

מספר נושא קובץ
15 Context API 15_context.md
16 Custom Hooks 16_custom_hooks.md
17 React Patterns (HOC, Error Boundaries) 17_patterns.md

חלק ד' - Routing & Performance

מספר נושא קובץ
18 React Router 18_routing.md
19 Performance (memo, useMemo, useCallback) 19_performance.md
20 Lazy Loading & Suspense 20_lazy_loading.md
21 useReducer 21_useReducer.md

חלק ה' - Redux & Testing

מספר נושא קובץ
22 Redux Toolkit Basics 22_redux.md
23 Redux Hooks & Async 23_redux_hooks.md
24 RTK Query 24_rtk_query.md
25 Testing (Jest & RTL) 25_testing.md

חלק ו' - React Native

מספר נושא קובץ
26 React Native עם Expo 26_react_native_expo.md

📁 תיקיית דוגמאות

מספר נושא קובץ
1 קומפוננטה ראשונה 01_hello_component.jsx
2 Props 02_props_example.jsx
3 State 03_state_example.jsx
4 אירועים 04_events_example.jsx
5 Hooks 05_hooks_example.jsx
6 רינדור מותנה 06_conditional_example.jsx
7 רשימות 07_lists_example.jsx
8 Todo App 08_todo_app.jsx
9 Lifecycle 09_lifecycle_example.jsx
10 Lifting State 10_lifting_state_example.jsx
11 Context API 11_context_example.jsx
12 Custom Hooks 12_custom_hooks_example.jsx
13 React Patterns 13_patterns_example.jsx
14 useReducer 14_useReducer_example.jsx
15 Performance 15_performance_example.jsx
16 Redux Toolkit 16_redux_example.jsx
17 React Native 17_react_native_example.jsx

🚀 איך להתחיל?

יצירת פרויקט React חדש

# עם Vite (מומלץ)
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev

התקנת חבילות נוספות

# React Router
npm install react-router-dom

# Redux Toolkit
npm install @reduxjs/toolkit react-redux

# Testing
npm install --save-dev @testing-library/react @testing-library/jest-dom

📖 סדר לימוד מומלץ

  1. שבוע 1-2: יסודות (פרקים 1-11)
  2. שבוע 3: Lifecycle & Component Patterns (פרקים 12-14)
  3. שבוע 4: Context & Custom Hooks (פרקים 15-17)
  4. שבוע 5: Routing & Performance (פרקים 18-21)
  5. שבוע 6: Redux & Testing (פרקים 22-25)

📋 נושאים שנכללים

  • ✅ JSX, Components, Props, State
  • ✅ Hooks: useState, useEffect, useRef, useMemo, useCallback, useReducer
  • ✅ Event Handling, Forms, Controlled Components
  • ✅ Conditional Rendering, Lists & Keys
  • ✅ Lifecycle, Memory Leaks, Cleanup
  • ✅ Props Drilling, Lifting State Up
  • ✅ Context API (Provider, Consumer, useContext)
  • ✅ Custom Hooks
  • ✅ HOC, Render Props, Error Boundaries
  • ✅ React Router (Dynamic Routes, Nested Routing)
  • ✅ Performance Optimization (React.memo, useMemo, useCallback)
  • ✅ Lazy Loading, Suspense, Code Splitting
  • ✅ Redux Toolkit (Store, Actions, Reducers)
  • ✅ useSelector, useDispatch, Async Actions
  • ✅ RTK Query
  • ✅ React Testing Library & Jest
  • ✅ React Native עם Expo

בהצלחה בלימוד React! 🎉

📜 רקע והיסטוריה של React

מהי React?

React היא ספריית JavaScript לבניית ממשקי משתמש (UI). היא פותחה בפייסבוק ושוחררה לציבור לראשונה בשנת 2013. הרעיון המרכזי שמאחוריה פשוט: לבנות את הממשק מיחידות קטנות ועצמאיות שנקראות קומפוננטות, ולהרכיב אותן יחד לממשק שלם ומורכב.

🎯 למה נוצרה React?

הצורך ב-React נולד מתוך כאב אמיתי. ככל שהממשקים בפייסבוק גדלו והפכו מורכבים יותר, ניהול ה-DOM באופן ידני הפך למסורבל ורגיש לשגיאות:

// בעיה: עדכון ידני של ה-DOM
document.getElementById('counter').innerHTML = newValue;
document.getElementById('total').innerHTML = calculateTotal();
document.getElementById('status').className = getStatusClass();
// ... וכן הלאה - קוד מסורבל ונפרד!
// React: הכל מתעדכן אוטומטית!
function Counter() {
    const [count, setCount] = useState(0);

    return (
        <div>
            <span>{count}</span>
            <button onClick={() => setCount(count + 1)}>+</button>
        </div>
    );
}

📅 ציר זמן

שנה אירוע
2011 פיתוח פנימי ב-Facebook
2013 שחרור React כקוד פתוח
2015 React Native לאפליקציות מובייל
2016 React 15 - שיפורי ביצועים
2019 React Hooks (גרסה 16.8)
2022 React 18 - Concurrent Features
2024 React 19 - Server Components

🌟 יתרונות מרכזיים

1. Virtual DOM

React מנהלת עותק וירטואלי של ה-DOM שנקרא Virtual DOM. במקום לגעת בעמוד עצמו בכל שינוי, היא מעדכנת תחילה את העותק הזה:

שינוי ב-State ──► Virtual DOM מתעדכן ──► השוואה ──► רק השינויים מעודכנים ב-DOM האמיתי

הגישה הזו חוסכת עדכונים מיותרים ומהירה בהרבה מעדכון ישיר של ה-DOM.

2. קומפוננטות (Components)

כל חלק בממשק הוא קומפוננטה עצמאית, שאחראית על מקטע מוגדר של התצוגה:

function App() {
    return (
        <div>
            <Header />
            <Sidebar />
            <MainContent />
            <Footer />
        </div>
    );
}

3. One-Way Data Flow

הנתונים ב-React זורמים בכיוון אחד בלבד - מקומפוננטת ההורה אל קומפוננטות הילד. זרימה חד-כיוונית זו מקלה על מעקב אחר מקור הנתונים ועל איתור תקלות:

App (state) ──► Header (props) ──► Logo (props)

4. JSX

תחביר שמאפשר לשלב קוד JavaScript ותגי HTML באותו הקובץ:

const element = <h1>שלום עולם!</h1>;
const dynamic = <p>היום הוא {new Date().toLocaleDateString('he-IL')}</p>;

🔗 React מול ספריות אחרות

תכונה React Vue Angular
סוג ספרייה Framework Framework
עקומת למידה בינונית קלה תלולה
גודל (gzip) ~40KB ~33KB ~143KB
שפה JavaScript/JSX JavaScript/Vue TypeScript
גיבוי Meta (Facebook) קהילה Google
שוק עבודה 🥇 הכי נפוץ 🥈 🥉

💻 מי משתמש ב-React?

כמעט כל חברת טכנולוגיה גדולה בונה חלק מהמוצרים שלה ב-React. הנה כמה דוגמאות מוכרות:

  • Facebook / Instagram - היוצרים!
  • Netflix - ממשק המשתמש
  • Airbnb - אתר ואפליקציה
  • WhatsApp Web - גרסת הווב
  • Dropbox - ממשק המשתמש
  • Discord - אפליקציית הצ'אט

📌 סיכום

React היא הספרייה המובילה כיום לפיתוח צד לקוח (Frontend). היא מציעה גישה דקלרטיבית לבניית ממשקים, ביצועים גבוהים בזכות ה-Virtual DOM וקהילה עצומה שמלווה כל שלב בדרך.

היא מתאימה במיוחד עבור:

  • אפליקציות עמוד יחיד (SPA - Single Page Application)
  • ממשקים אינטראקטיביים שמגיבים בזמן אמת
  • פרויקטים גדולים שמפותחים בעבודת צוות

הבא: התקנה והגדרות →

🔧 התקנה והגדרות

דרישות מקדימות

1. Node.js

הורידו והתקינו את Node.js מהאתר הרשמי: nodejs.org. Node.js כולל גם את מנהל החבילות npm, שבו נשתמש להתקנת ספריות לאורך הקורס.

לאחר ההתקנה, ודאו שהכול תקין בעזרת הפקודות הבאות:

node --version   # לפחות v22
npm --version    # לפחות v11

2. עורך קוד

העורך המומלץ הוא Visual Studio Code. כדאי להתקין בו את התוספים הבאים כדי לייעל את העבודה:

  • ES7+ React/Redux/React-Native snippets - קיצורי דרך לכתיבת קומפוננטות ו-Hooks
  • Prettier - עיצוב אוטומטי ואחיד של הקוד
  • Auto Rename Tag - שינוי שם תג הפתיחה והסגירה בו-זמנית

🚀 יצירת פרויקט React

שיטה מומלצת: Vite

# יצירת פרויקט חדש
npm create vite@latest my-react-app -- --template react

# כניסה לתיקייה
cd my-react-app

# התקנת חבילות
npm install

# הרצת שרת פיתוח
npm run dev

כעת פתחו את הדפדפן בכתובת http://localhost:5173 - האפליקציה שלכם רצה.

שיטה חלופית: Create React App

npx create-react-app my-app
cd my-app
npm start

⚠️ הערה: Vite מהיר משמעותית וזו הבחירה המומלצת לכל פרויקט חדש. Create React App נחשב כיום לכלי מיושן.


📁 מבנה הפרויקט (Vite)

my-react-app/
├── node_modules/          # חבילות npm
├── public/                # קבצים סטטיים
│   └── vite.svg
├── src/                   # קוד המקור
│   ├── assets/            # תמונות ומשאבים
│   ├── App.css            # סגנונות הקומפוננטה הראשית
│   ├── App.jsx            # קומפוננטה ראשית
│   ├── index.css          # סגנונות גלובליים
│   └── main.jsx           # נקודת הכניסה
├── .gitignore
├── index.html             # דף ה-HTML הראשי
├── package.json           # הגדרות הפרויקט
└── vite.config.js         # הגדרות Vite

📄 קבצים חשובים

index.html

<!DOCTYPE html>
<html lang="he" dir="rtl">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>האפליקציה שלי</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

App.jsx

import './App.css'

function App() {
  return (
    <div>
      <h1>שלום עולם! 👋</h1>
      <p>זו אפליקציית React הראשונה שלי</p>
    </div>
  )
}

export default App

🛠️ פקודות נפוצות

פקודה תיאור
npm run dev הרצת שרת פיתוח
npm run build בניית גרסת production
npm run preview צפייה בגרסת production
npm install <package> התקנת חבילה

📦 חבילות נפוצות

לצד React עצמה, רוב הפרויקטים נשענים על מספר חבילות משלימות. הנה הנפוצות ביותר, לפי תחום השימוש:

# ניווט (Routing)
npm install react-router-dom

# ניהול state גלובלי
npm install zustand
# או
npm install @reduxjs/toolkit react-redux

# סטיילינג
npm install styled-components
# או
npm install @emotion/react @emotion/styled

# אייקונים
npm install react-icons

# HTTP requests
npm install axios

⚙️ הגדרות מומלצות

vite.config.js עם alias

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
    }
  }
})

.eslintrc.cjs (לבדיקת קוד)

module.exports = {
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
  ],
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
  settings: { react: { version: 'detect' } },
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
  },
}

📝 תחביר JSX

מהו JSX?

JSX (JavaScript XML) הוא תחביר שמאפשר לכתוב קוד שנראה כמו HTML ישירות בתוך JavaScript. חשוב להבין שזה אינו HTML אמיתי - לפני שהקוד מגיע לדפדפן הוא מתקמפל ל-JavaScript רגיל. JSX הוא בסך הכול "סוכר תחבירי" שהופך את בניית הממשק לקריאה ונוחה יותר.

// JSX
const element = <h1>שלום עולם!</h1>;

// מה שזה הופך להיות (React.createElement)
const element = React.createElement('h1', null, 'שלום עולם!');

📌 כללי תחביר בסיסיים

1. אלמנט שורש יחיד

כל קומפוננטה חייבת להחזיר אלמנט עוטף אחד בלבד. אפשר לעטוף את התוכן ב-div, ואם לא רוצים להוסיף תגית מיותרת ל-DOM - להשתמש ב-Fragment:

// ❌ שגוי - שני אלמנטים שורש
function Bad() {
    return (
        <h1>כותרת</h1>
        <p>תוכן</p>
    );
}

// ✅ נכון - עטיפה ב-div
function Good() {
    return (
        <div>
            <h1>כותרת</h1>
            <p>תוכן</p>
        </div>
    );
}

// ✅ נכון - שימוש ב-Fragment
function Better() {
    return (
        <>
            <h1>כותרת</h1>
            <p>תוכן</p>
        </>
    );
}

2. סגירת תגיות

כל התגיות חייבות להיסגר:

// ✅ תגיות עם תוכן
<div>תוכן</div>
<p>פסקה</p>

// ✅ תגיות self-closing
<img src="image.jpg" />
<br />
<input type="text" />
<hr />

3. camelCase לתכונות

מכיוון ש-JSX הוא בעצם JavaScript, חלק משמות התכונות משתנים לעומת HTML רגיל ונכתבים ב-camelCase. שימו לב במיוחד למילים השמורות class ו-for, שהופכות ל-className ו-htmlFor:

// HTML רגיל          // JSX
// class="..."    →   className="..."
// for="..."      →   htmlFor="..."
// onclick="..."  →   onClick={...}
// tabindex="..." →   tabIndex={...}

<label htmlFor="name" className="label">
    שם:
    <input id="name" tabIndex={1} />
</label>

🔤 ביטויים בסוגריים מסולסלים

הסוגריים המסולסלים {} הם הגשר בין ה-JSX ל-JavaScript: כל ביטוי שנכתב בתוכם מחושב, והתוצאה מוצגת במסך. אפשר להכניס אליהם משתנים, חישובים, קריאות לפונקציות ותנאים:

משתנים

const name = "דני";
const age = 25;

return (
    <div>
        <h1>שלום {name}!</h1>
        <p>גיל: {age}</p>
    </div>
);

ביטויים חשבוניים

return (
    <div>
        <p>2 + 2 = {2 + 2}</p>
        <p>מספר אקראי: {Math.random()}</p>
        <p>גיל בעוד 10 שנים: {age + 10}</p>
    </div>
);

קריאות לפונקציות

function formatDate(date) {
    return date.toLocaleDateString('he-IL');
}

return <p>תאריך: {formatDate(new Date())}</p>;

ביטויי תנאי (Ternary)

const isLoggedIn = true;

return (
    <div>
        {isLoggedIn ? <p>שלום משתמש!</p> : <p>אנא התחבר</p>}
    </div>
);

אובייקטים ומערכים

const user = { name: "דני", age: 25 };
const colors = ["אדום", "ירוק", "כחול"];

return (
    <div>
        <p>שם: {user.name}</p>
        <p>צבע ראשון: {colors[0]}</p>
    </div>
);

🎨 סגנונות (Styles)

Inline Styles

// סגנונות כאובייקט JavaScript
const style = {
    color: 'blue',
    fontSize: '20px',        // camelCase!
    backgroundColor: '#f0f0f0',
    padding: '10px'
};

return <p style={style}>טקסט מעוצב</p>;

// או inline
return (
    <p style={{ color: 'red', fontWeight: 'bold' }}>
        טקסט אדום ומודגש
    </p>
);

CSS Classes

// App.css
// .title { color: blue; font-size: 24px; }
// .highlight { background-color: yellow; }

import './App.css';

function App() {
    return (
        <h1 className="title highlight">כותרת</h1>
    );
}

Classes דינמיים

const isActive = true;
const isError = false;

return (
    <div className={`box ${isActive ? 'active' : ''} ${isError ? 'error' : ''}`}>
        תוכן
    </div>
);

🧩 Fragments

Fragment מאפשר לקבץ כמה אלמנטים יחד מבלי להוסיף תגית עוטפת מיותרת ל-DOM. הוא פותר בדיוק את מגבלת "אלמנט השורש היחיד", בלי לזהם את מבנה העמוד ב-div נוסף:

import { Fragment } from 'react';

// תחביר מלא
function List() {
    return (
        <Fragment>
            <li>פריט 1</li>
            <li>פריט 2</li>
            <li>פריט 3</li>
        </Fragment>
    );
}

// תחביר מקוצר (נפוץ יותר)
function List() {
    return (
        <>
            <li>פריט 1</li>
            <li>פריט 2</li>
            <li>פריט 3</li>
        </>
    );
}

// עם key (רק בתחביר המלא)
function Items({ items }) {
    return items.map(item => (
        <Fragment key={item.id}>
            <dt>{item.term}</dt>
            <dd>{item.description}</dd>
        </Fragment>
    ));
}

💬 הערות ב-JSX

function App() {
    return (
        <div>
            {/* זו הערה ב-JSX */}
            <h1>כותרת</h1>

            {/* 
                הערה
                בכמה
                שורות 
            */}
            <p>תוכן</p>
        </div>
    );
}

⚠️ דברים שאי אפשר לעשות ב-JSX

בתוך הסוגריים המסולסלים אפשר לכתוב ביטויים (expressions) בלבד - כלומר קוד שמחזיר ערך. משפטי בקרה כמו if או לולאת for אינם ביטויים, ולכן במקומם משתמשים ב-ternary, באופרטור && ובמתודה map:

// ❌ שגוי - if/else רגיל לא עובד בתוך JSX
function Bad() {
    return (
        <div>
            {if (condition) { <p>כן</p> }}  // שגיאה!
        </div>
    );
}

// ✅ השתמשו ב-ternary או &&
function Good() {
    return (
        <div>
            {condition ? <p>כן</p> : <p>לא</p>}
            {condition && <p>מוצג רק אם true</p>}
        </div>
    );
}

// ❌ for loop ישירות - לא עובד
function Bad() {
    return (
        <ul>
            {for (let i = 0; i < 3; i++) { <li>{i}</li> }}  // שגיאה!
        </ul>
    );
}

// ✅ השתמשו ב-map
function Good() {
    return (
        <ul>
            {[0, 1, 2].map(i => <li key={i}>{i}</li>)}
        </ul>
    );
}

📋 סיכום תחביר JSX

כלל דוגמה
אלמנט שורש יחיד <> ... </>
סגירת תגיות <img />
className <div className="box">
htmlFor <label htmlFor="id">
ביטויים בסוגריים {variable}
Inline styles style={{ color: 'red' }}
הערות {/* הערה */}

הקודם: ← התקנה והגדרות
הבא: קומפוננטות →

🧩 קומפוננטות (Components)

מהי קומפוננטה?

קומפוננטה היא יחידת בניין עצמאית וניתנת לשימוש חוזר בממשק המשתמש. אפשר לחשוב עליה כמו על קוביית לגו: כל קומפוננטה אחראית על חלק מוגדר בממשק, ומחיבור של קומפוננטות פשוטות אנחנו בונים ממשקים שלמים ומורכבים. בפועל, קומפוננטה היא פשוט פונקציית JavaScript שמחזירה JSX.

// קומפוננטה = פונקציה שמחזירה JSX
function Button() {
    return <button>לחץ עליי</button>;
}

// שימוש בקומפוננטה
function App() {
    return (
        <div>
            <Button />
            <Button />
            <Button />
        </div>
    );
}

📌 סוגי קומפוננטות

Function Components (מומלץ!)

// קומפוננטה פונקציונלית - הסטנדרט היום
function Welcome() {
    return <h1>ברוכים הבאים!</h1>;
}

// או כ-Arrow Function
const Welcome = () => {
    return <h1>ברוכים הבאים!</h1>;
};

// או בקיצור (implicit return)
const Welcome = () => <h1>ברוכים הבאים!</h1>;

Class Components (ישן - לידע כללי)

// קומפוננטת מחלקה - סגנון ישן
import { Component } from 'react';

class Welcome extends Component {
    render() {
        return <h1>ברוכים הבאים!</h1>;
    }
}

💡 טיפ: העדיפו תמיד Function Components. הם קצרים וקריאים יותר, וזו הדרך היחידה לעבוד עם Hooks. קומפוננטות מחלקה (Class Components) מובאות כאן להיכרות בלבד, למקרה שתיתקלו בהן בקוד ישן.


📁 מבנה קובץ קומפוננטה

קומפוננטה בסיסית

// src/components/Header.jsx

function Header() {
    return (
        <header>
            <h1>האתר שלי</h1>
            <nav>
                <a href="/">בית</a>
                <a href="/about">אודות</a>
            </nav>
        </header>
    );
}

export default Header;

קומפוננטה עם סגנונות

// src/components/Card.jsx
import './Card.css';

function Card() {
    return (
        <div className="card">
            <h2 className="card-title">כותרת</h2>
            <p className="card-content">תוכן הכרטיס</p>
        </div>
    );
}

export default Card;

📤 ייצוא וייבוא

Default Export

// Button.jsx - ייצוא
function Button() {
    return <button>לחץ</button>;
}
export default Button;

// App.jsx - ייבוא
import Button from './components/Button';
// אפשר לקרוא לזה בכל שם
import MyButton from './components/Button';

Named Export

// components.jsx - ייצוא
export function Button() {
    return <button>לחץ</button>;
}

export function Input() {
    return <input type="text" />;
}

// App.jsx - ייבוא
import { Button, Input } from './components';
// חייב להשתמש בשם המדויק

שילוב

// Button.jsx
export function SmallButton() {
    return <button className="small">קטן</button>;
}

function Button() {
    return <button>לחץ</button>;
}

export default Button;

// App.jsx
import Button, { SmallButton } from './components/Button';

🏗️ מבנה תיקיות מומלץ

src/
├── components/
│   ├── common/           # קומפוננטות משותפות
│   │   ├── Button.jsx
│   │   ├── Button.css
│   │   ├── Input.jsx
│   │   └── Modal.jsx
│   ├── layout/           # קומפוננטות מבנה
│   │   ├── Header.jsx
│   │   ├── Footer.jsx
│   │   └── Sidebar.jsx
│   └── features/         # קומפוננטות לפי פיצ'ר
│       ├── UserProfile.jsx
│       └── ProductCard.jsx
├── pages/                # דפים
│   ├── Home.jsx
│   └── About.jsx
├── App.jsx
└── main.jsx

🔗 קינון קומפוננטות

הכוח האמיתי של React מתגלה כשמקננים קומפוננטות זו בתוך זו. קומפוננטות קטנות ופשוטות משמשות אבני בניין לקומפוננטות גדולות יותר, עד לאפליקציה שלמה:

// קומפוננטות קטנות
function Logo() {
    return <img src="/logo.png" alt="לוגו" />;
}

function NavLinks() {
    return (
        <nav>
            <a href="/">בית</a>
            <a href="/about">אודות</a>
            <a href="/contact">צור קשר</a>
        </nav>
    );
}

// קומפוננטה מורכבת
function Header() {
    return (
        <header className="header">
            <Logo />
            <NavLinks />
        </header>
    );
}

// אפליקציה שלמה
function App() {
    return (
        <div>
            <Header />
            <main>
                <h1>ברוכים הבאים</h1>
            </main>
            <Footer />
        </div>
    );
}

✅ כללים לשמות קומפוננטות

  1. PascalCase - אות גדולה בתחילת כל מילה:

    // ✅ נכון
    function UserProfile() { }
    function ShoppingCart() { }
    
    // ❌ שגוי
    function userProfile() { }
    function shopping_cart() { }
  2. שם תיאורי - מתאר את מה שהקומפוננטה עושה:

    // ✅ נכון
    function ProductCard() { }
    function LoginForm() { }
    
    // ❌ לא ברור
    function Card1() { }
    function Component() { }
  3. קובץ = שם הקומפוננטה:

    UserProfile.jsx  →  function UserProfile() { }
    LoginForm.jsx    →  function LoginForm() { }

📋 סיכום

נושא המלצה
סוג קומפוננטה Function Component
ייצוא export default לקומפוננטה הראשית
שם PascalCase
קובץ שם זהה לקומפוננטה
CSS קובץ נפרד או CSS Modules

הקודם: ← תחביר JSX
הבא: Props →

📦 Props

מהם Props?

Props (קיצור של Properties) הם הדרך להעביר נתונים מקומפוננטת הורה אל קומפוננטת ילד. הם מתפקדים בדיוק כמו פרמטרים של פונקציה: אותה קומפוננטה יכולה להציג תוכן שונה בכל פעם, בהתאם ל-props שהיא מקבלת. כך הופכים קומפוננטה סטטית לקומפוננטה גמישה וניתנת לשימוש חוזר.

// בלי Props - קומפוננטה סטטית
function Greeting() {
    return <h1>שלום דני!</h1>;
}

// עם Props - קומפוננטה דינמית
function Greeting(props) {
    return <h1>שלום {props.name}!</h1>;
}

// שימוש
<Greeting name="דני" />
<Greeting name="רונית" />
<Greeting name="משה" />

📌 העברת Props

טיפוסים בסיסיים

function UserCard(props) {
    return (
        <div className="card">
            <h2>{props.name}</h2>
            <p>גיל: {props.age}</p>
            <p>עיר: {props.city}</p>
        </div>
    );
}

// שימוש
<UserCard name="דני" age={25} city="תל אביב" />
<UserCard name="רונית" age={30} city="ירושלים" />

Destructuring (מומלץ!)

// במקום props.name, props.age...
function UserCard({ name, age, city }) {
    return (
        <div className="card">
            <h2>{name}</h2>
            <p>גיל: {age}</p>
            <p>עיר: {city}</p>
        </div>
    );
}

🔢 סוגי Props

מחרוזות (Strings)

// מחרוזות בגרשיים
<Button text="לחץ עליי" />
<Title message="ברוכים הבאים" />

מספרים (Numbers)

// מספרים בסוגריים מסולסלים
<Counter initial={0} />
<Product price={99.99} quantity={3} />

בוליאנים (Booleans)

// true - מספיק לכתוב את השם
<Button disabled />
<Modal isOpen />

// false - צריך לציין במפורש
<Button disabled={false} />
<Modal isOpen={false} />

מערכים ואובייקטים

const colors = ['אדום', 'ירוק', 'כחול'];
const user = { name: 'דני', age: 25 };

<ColorList colors={colors} />
<Profile user={user} />

פונקציות

function handleClick() {
    alert('לחצת!');
}

<Button onClick={handleClick} />

👶 Children Props

children הוא prop מיוחד שמכיל את כל מה שנכתב בין תגית הפתיחה לתגית הסגירה של הקומפוננטה. הוא מאפשר לבנות קומפוננטות "עוטפות" גמישות, כמו כרטיס או חלון קופץ, שלא יודעות מראש איזה תוכן ייכנס אליהן:

function Card({ children, title }) {
    return (
        <div className="card">
            <h2>{title}</h2>
            <div className="card-content">
                {children}
            </div>
        </div>
    );
}

// שימוש
<Card title="כותרת">
    <p>זה התוכן שבפנים!</p>
    <button>כפתור</button>
</Card>

<Card title="כותרת אחרת">
    <ul>
        <li>פריט 1</li>
        <li>פריט 2</li>
    </ul>
</Card>

🎯 Default Props

לעיתים נרצה שלקומפוננטה יהיה ערך התחלתי סביר גם כשההורה לא מעביר prop מסוים. לשם כך מגדירים ערכי ברירת מחדל. הדרך המומלצת כיום היא לקבוע אותם ישירות ב-destructuring:

שיטה 1: Destructuring עם ברירת מחדל

function Button({ text = "לחץ", color = "blue", size = "medium" }) {
    return (
        <button className={`btn btn-${color} btn-${size}`}>
            {text}
        </button>
    );
}

// שימוש
<Button />                          // text="לחץ", color="blue", size="medium"
<Button text="שמור" />              // text="שמור", color="blue", size="medium"
<Button text="מחק" color="red" />   // text="מחק", color="red", size="medium"

שיטה 2: defaultProps (ישן יותר)

function Button({ text, color, size }) {
    return (
        <button className={`btn btn-${color} btn-${size}`}>
            {text}
        </button>
    );
}

Button.defaultProps = {
    text: "לחץ",
    color: "blue",
    size: "medium"
};

📐 Spread Operator עם Props

const buttonProps = {
    text: "לחץ",
    color: "blue",
    onClick: handleClick,
    disabled: false
};

// במקום לכתוב כל prop בנפרד
<Button text={buttonProps.text} color={buttonProps.color} ... />

// אפשר להשתמש ב-spread
<Button {...buttonProps} />

// שילוב: spread + override
<Button {...buttonProps} color="red" />  // color יהיה "red"

⚠️ Props הם Read-Only

עיקרון חשוב ב-React: אסור לשנות את ה-props בתוך הקומפוננטה שמקבלת אותם. ה-props שייכים להורה, והקומפוננטה מקבלת אותם לקריאה בלבד. שמירה על העיקרון הזה היא מה שהופך את זרימת הנתונים לצפויה וקלה למעקב:

function Counter({ count }) {
    // ❌ שגיאה! אי אפשר לשנות props
    count = count + 1;

    return <p>ספירה: {count}</p>;
}

כשצריך ערך שמשתנה לאורך זמן בתוך הקומפוננטה עצמה, משתמשים ב-State - הנושא של השיעור הבא.


🔄 העברת Props בשרשרת

// App -> Header -> Logo

function App() {
    const logoSrc = "/logo.png";
    return <Header logo={logoSrc} />;
}

function Header({ logo }) {
    return (
        <header>
            <Logo src={logo} />
            <nav>...</nav>
        </header>
    );
}

function Logo({ src }) {
    return <img src={src} alt="לוגו" />;
}

💡 טיפ: העברת אותו prop דרך רמות רבות (תופעה שמכונה Props Drilling) הופכת את הקוד למסורבל. במקרים כאלה שקלו להשתמש ב-Context API, שנלמד בהמשך.


📋 סיכום

נושא תחביר
העברת מחרוזת <Component name="value" />
העברת מספר <Component count={5} />
העברת boolean <Component active />
העברת אובייקט <Component user={{ name: "..." }} />
העברת פונקציה <Component onClick={handleClick} />
Children <Component>תוכן</Component>
ברירת מחדל function C({ prop = "default" })
Spread <Component {...props} />

הקודם: ← קומפוננטות
הבא: State →

🔄 State

מהו State?

State הוא נתון פנימי של קומפוננטה שיכול להשתנות לאורך זמן - למשל ספירה שעולה, טקסט שהמשתמש מקליד או חלון שנפתח ונסגר. זהו ההבדל המהותי מ-props: את ה-State הקומפוננטה מנהלת בעצמה. הכוח שלו טמון בכך שבכל פעם שה-State משתנה, React מרנדרת את הקומפוננטה מחדש ומעדכנת את המסך אוטומטית.

import { useState } from 'react';

function Counter() {
    // הגדרת state עם ערך התחלתי 0
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>ספירה: {count}</p>
            <button onClick={() => setCount(count + 1)}>
                הוסף 1
            </button>
        </div>
    );
}

📌 useState Hook

התחביר

const [value, setValue] = useState(initialValue);
//     ↑       ↑                      ↑
//   ערך    פונקציית           ערך התחלתי
//  נוכחי    עדכון

דוגמאות

// מספר
const [count, setCount] = useState(0);

// מחרוזת
const [name, setName] = useState("");

// בוליאני
const [isOpen, setIsOpen] = useState(false);

// מערך
const [items, setItems] = useState([]);

// אובייקט
const [user, setUser] = useState({ name: "", age: 0 });

⚡ עדכון State

עדכון ישיר

function Counter() {
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(5)}>הגדר ל-5</button>
            <button onClick={() => setCount(count + 1)}>+1</button>
            <button onClick={() => setCount(count - 1)}>-1</button>
        </div>
    );
}

עדכון מבוסס ערך קודם (מומלץ!)

function Counter() {
    const [count, setCount] = useState(0);

    // ✅ מומלץ - שימוש בפונקציית callback
    const increment = () => setCount(prev => prev + 1);
    const decrement = () => setCount(prev => prev - 1);
    const double = () => setCount(prev => prev * 2);

    return (
        <div>
            <p>{count}</p>
            <button onClick={increment}>+1</button>
            <button onClick={decrement}>-1</button>
            <button onClick={double}>×2</button>
        </div>
    );
}

📝 State עם טפסים

function LoginForm() {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log("Email:", email);
        console.log("Password:", password);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input 
                type="email" 
                value={email} 
                onChange={(e) => setEmail(e.target.value)}
                placeholder="אימייל"
            />
            <input 
                type="password" 
                value={password} 
                onChange={(e) => setPassword(e.target.value)}
                placeholder="סיסמה"
            />
            <button type="submit">התחבר</button>
        </form>
    );
}

📦 State עם אובייקטים

function UserForm() {
    const [user, setUser] = useState({
        name: "",
        email: "",
        age: 0
    });

    // ❌ שגוי - לא לשנות ישירות
    // user.name = "דני";

    // ✅ נכון - יצירת אובייקט חדש
    const updateName = (name) => {
        setUser({ ...user, name: name });
    };

    // ✅ או בגרסה מקוצרת
    const updateEmail = (email) => {
        setUser(prev => ({ ...prev, email }));
    };

    return (
        <div>
            <input 
                value={user.name}
                onChange={(e) => updateName(e.target.value)}
            />
            <input 
                value={user.email}
                onChange={(e) => updateEmail(e.target.value)}
            />
        </div>
    );
}

📚 State עם מערכים

function TodoList() {
    const [todos, setTodos] = useState([
        { id: 1, text: "לקנות חלב", done: false },
        { id: 2, text: "לשלם חשבונות", done: false }
    ]);

    // הוספת פריט
    const addTodo = (text) => {
        setTodos(prev => [...prev, {
            id: Date.now(),
            text,
            done: false
        }]);
    };

    // מחיקת פריט
    const deleteTodo = (id) => {
        setTodos(prev => prev.filter(todo => todo.id !== id));
    };

    // עדכון פריט
    const toggleTodo = (id) => {
        setTodos(prev => prev.map(todo => 
            todo.id === id 
                ? { ...todo, done: !todo.done }
                : todo
        ));
    };

    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id}>
                    <span 
                        style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
                        onClick={() => toggleTodo(todo.id)}
                    >
                        {todo.text}
                    </span>
                    <button onClick={() => deleteTodo(todo.id)}>❌</button>
                </li>
            ))}
        </ul>
    );
}

🔀 State vs Props

תכונה Props State
מקור מגיע מההורה פנימי לקומפוננטה
שינוי לא ניתן לשנות ניתן לשנות עם setter
מטרה העברת נתונים ניהול נתונים משתנים
רינדור מחדש כשההורה מתרנדר כש-State משתנה
// Props - נתונים מבחוץ (read-only)
function Greeting({ name }) {
    return <h1>שלום {name}!</h1>;
}

// State - נתונים פנימיים (mutable)
function Counter() {
    const [count, setCount] = useState(0);
    return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// שילוב: State בהורה, Props לילד
function App() {
    const [name, setName] = useState("דני");
    return (
        <div>
            <input value={name} onChange={(e) => setName(e.target.value)} />
            <Greeting name={name} />  {/* State הופך ל-Props */}
        </div>
    );
}

⚠️ כללים חשובים

1. אל תשנו State ישירות

// ❌ שגוי
count = count + 1;
user.name = "דני";
todos.push({ id: 1, text: "חדש" });

// ✅ נכון
setCount(count + 1);
setUser({ ...user, name: "דני" });
setTodos([...todos, { id: 1, text: "חדש" }]);

2. State הוא אסינכרוני

קריאה ל-setter אינה מעדכנת את הערך באופן מיידי, אלא מתזמנת רינדור מחדש. לכן מיד אחרי הקריאה המשתנה עדיין מחזיק את הערך הישן. אם צריך לחשב על בסיס הערך העדכני, משתמשים בגרסת ה-callback (prev => ...):

function Example() {
    const [count, setCount] = useState(0);

    const handleClick = () => {
        setCount(count + 1);
        console.log(count); // עדיין הערך הישן!
    };
}

3. מספר useState בקומפוננטה אחת

function Form() {
    const [name, setName] = useState("");
    const [email, setEmail] = useState("");
    const [age, setAge] = useState(0);
    const [isValid, setIsValid] = useState(false);
    // ...
}

📋 סיכום

פעולה קוד
הגדרת State const [x, setX] = useState(initial)
עדכון ישיר setX(newValue)
עדכון מבוסס קודם setX(prev => prev + 1)
עדכון אובייקט setX(prev => ({...prev, key: val}))
הוספה למערך setX(prev => [...prev, newItem])
מחיקה ממערך setX(prev => prev.filter(...))
עדכון במערך setX(prev => prev.map(...))

הקודם: ← Props
הבא: אירועים →

🎯 אירועים (Events)

מבוא

אירועים הם הדרך שבה הממשק מגיב לפעולות המשתמש - לחיצה, הקלדה, מעבר עכבר ועוד. React משתמשת במערכת אירועים דומה מאוד לזו של HTML, אך עם שני הבדלים מרכזיים:

  • שמות האירועים נכתבים ב-camelCase (למשל onClick ולא onclick)
  • מעבירים פונקציה כערך, ולא מחרוזת של קוד
// HTML רגיל
<button onclick="handleClick()">לחץ</button>

// React
<button onClick={handleClick}>לחץ</button>

📌 אירועים נפוצים

onClick - לחיצה

function Button() {
    const handleClick = () => {
        alert('לחצת על הכפתור!');
    };

    return <button onClick={handleClick}>לחץ עליי</button>;
}

// או inline
<button onClick={() => alert('לחצת!')}>לחץ</button>

onChange - שינוי ערך

function Input() {
    const [text, setText] = useState("");

    const handleChange = (event) => {
        setText(event.target.value);
    };

    return (
        <input 
            type="text" 
            value={text} 
            onChange={handleChange}
            placeholder="הקלד משהו..."
        />
    );
}

onSubmit - שליחת טופס

function Form() {
    const [name, setName] = useState("");

    const handleSubmit = (event) => {
        event.preventDefault(); // מונע רענון הדף
        console.log('שם:', name);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input 
                value={name} 
                onChange={(e) => setName(e.target.value)} 
            />
            <button type="submit">שלח</button>
        </form>
    );
}

🔧 רשימת אירועים

אירועי עכבר

<div
    onClick={() => console.log('לחיצה')}
    onDoubleClick={() => console.log('לחיצה כפולה')}
    onMouseEnter={() => console.log('עכבר נכנס')}
    onMouseLeave={() => console.log('עכבר יצא')}
    onMouseMove={(e) => console.log(e.clientX, e.clientY)}
>
    עבור עם העכבר
</div>

אירועי מקלדת

<input
    onKeyDown={(e) => console.log('מקש למטה:', e.key)}
    onKeyUp={(e) => console.log('מקש למעלה:', e.key)}
    onKeyPress={(e) => {
        if (e.key === 'Enter') {
            console.log('Enter נלחץ!');
        }
    }}
/>

אירועי פוקוס

<input
    onFocus={() => console.log('קיבל פוקוס')}
    onBlur={() => console.log('איבד פוקוס')}
/>

אירועי טופס

<form onSubmit={handleSubmit} onReset={handleReset}>
    <input onChange={handleChange} />
    <select onChange={handleSelect} />
    <textarea onChange={handleTextChange} />
</form>

📦 Event Object

כל פונקציית handler מקבלת אוטומטית אובייקט event שמתאר את האירוע שהתרחש. דרכו ניתן לגשת לאלמנט שהפעיל את האירוע, לערך שהוזן, למקש שנלחץ ולמידע נוסף:

function EventExample() {
    const handleClick = (event) => {
        console.log('סוג האירוע:', event.type);
        console.log('אלמנט מקור:', event.target);
        console.log('מיקום X:', event.clientX);
        console.log('מיקום Y:', event.clientY);
        console.log('מקש Ctrl לחוץ?', event.ctrlKey);
    };

    const handleInput = (event) => {
        console.log('ערך חדש:', event.target.value);
        console.log('שם השדה:', event.target.name);
    };

    return (
        <div>
            <button onClick={handleClick}>לחץ</button>
            <input name="username" onChange={handleInput} />
        </div>
    );
}

🛑 preventDefault ו-stopPropagation

שתי מתודות של אובייקט האירוע שכדאי להכיר: preventDefault מבטלת את התנהגות ברירת המחדל של הדפדפן (כמו רענון הדף בשליחת טופס או ניווט בלחיצה על קישור), ו-stopPropagation עוצרת את בעבוע האירוע כלפי מעלה, כך שאירוע בקומפוננטה פנימית לא יפעיל בטעות handler של קומפוננטה עוטפת:

// preventDefault - מונע התנהגות ברירת מחדל
function Link() {
    const handleClick = (e) => {
        e.preventDefault(); // מונע ניווט
        console.log('הלינק נלחץ');
    };

    return <a href="https://google.com" onClick={handleClick}>לינק</a>;
}

// stopPropagation - עוצר את בועת האירוע
function Nested() {
    return (
        <div onClick={() => console.log('חיצוני')}>
            <button onClick={(e) => {
                e.stopPropagation();
                console.log('פנימי');
            }}>
                לחץ
            </button>
        </div>
    );
    // בלי stopPropagation: "פנימי" ואז "חיצוני"
    // עם stopPropagation: רק "פנימי"
}

📤 העברת פרמטרים נוספים

שיטה 1: Arrow Function

function List() {
    const items = ['תפוח', 'בננה', 'תפוז'];

    const handleClick = (item) => {
        console.log('נבחר:', item);
    };

    return (
        <ul>
            {items.map(item => (
                <li key={item}>
                    <button onClick={() => handleClick(item)}>
                        {item}
                    </button>
                </li>
            ))}
        </ul>
    );
}

שיטה 2: data attributes

function List() {
    const handleClick = (event) => {
        const item = event.target.dataset.item;
        console.log('נבחר:', item);
    };

    return (
        <ul>
            {['תפוח', 'בננה', 'תפוז'].map(item => (
                <li key={item}>
                    <button data-item={item} onClick={handleClick}>
                        {item}
                    </button>
                </li>
            ))}
        </ul>
    );
}

🔄 אירוע + State

function Counter() {
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>ספירה: {count}</p>
            <button onClick={() => setCount(c => c + 1)}>הוסף</button>
            <button onClick={() => setCount(c => c - 1)}>הפחת</button>
            <button onClick={() => setCount(0)}>אפס</button>
        </div>
    );
}

function Toggle() {
    const [isOn, setIsOn] = useState(false);

    return (
        <button onClick={() => setIsOn(prev => !prev)}>
            {isOn ? '🔵 פועל' : '⚪ כבוי'}
        </button>
    );
}

📝 דוגמה מלאה: טופס

function ContactForm() {
    const [formData, setFormData] = useState({
        name: '',
        email: '',
        message: ''
    });
    const [submitted, setSubmitted] = useState(false);

    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData(prev => ({
            ...prev,
            [name]: value
        }));
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('נתוני הטופס:', formData);
        setSubmitted(true);
    };

    if (submitted) {
        return <p>תודה {formData.name}! ההודעה נשלחה.</p>;
    }

    return (
        <form onSubmit={handleSubmit}>
            <input
                name="name"
                value={formData.name}
                onChange={handleChange}
                placeholder="שם"
            />
            <input
                name="email"
                type="email"
                value={formData.email}
                onChange={handleChange}
                placeholder="אימייל"
            />
            <textarea
                name="message"
                value={formData.message}
                onChange={handleChange}
                placeholder="הודעה"
            />
            <button type="submit">שלח</button>
        </form>
    );
}

📋 סיכום אירועים

אירוע שימוש
onClick לחיצה על אלמנט
onChange שינוי ערך (input, select)
onSubmit שליחת טופס
onKeyDown לחיצת מקש
onMouseEnter עכבר נכנס
onMouseLeave עכבר יוצא
onFocus קבלת פוקוס
onBlur איבוד פוקוס

הקודם: ← State
הבא: Hooks →

🪝 Hooks

מהם Hooks?

Hooks הם פונקציות מיוחדות שמאפשרות "להתחבר" ליכולות הליבה של React - ניהול State, תופעות לוואי, גישה ל-DOM ועוד - ישירות מתוך Function Components. עד שנוספו בגרסה 16.8 (שנת 2019), יכולות אלו היו זמינות רק בקומפוננטות מחלקה. ה-Hooks הם שהפכו את הקומפוננטות הפונקציונליות לדרך העבודה המקובלת ב-React.

import { useState, useEffect } from 'react';

function Counter() {
    // useState הוא Hook!
    const [count, setCount] = useState(0);

    return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

📌 חוקי Hooks

React מזהה את ה-Hooks לפי הסדר שבו הם נקראים בכל רינדור. כדי שהסדר יישאר קבוע, יש לשמור על שני כללים ברזל:

1. רק ברמה העליונה

אין לקרוא ל-Hook בתוך תנאי, לולאה או פונקציה מקוננת - רק ישירות בגוף הקומפוננטה. כך React מבטיחה שאותם Hooks נקראים באותו סדר בכל רינדור:

// ✅ נכון - ברמה העליונה
function Component() {
    const [count, setCount] = useState(0);

    if (count > 5) {
        // ...לוגיקה
    }
}

// ❌ שגוי - בתוך תנאי
function Component() {
    if (someCondition) {
        const [count, setCount] = useState(0); // אסור!
    }
}

// ❌ שגוי - בתוך לולאה
function Component() {
    for (let i = 0; i < 3; i++) {
        const [x, setX] = useState(0); // אסור!
    }
}

2. רק בקומפוננטות React (או Custom Hooks)

// ✅ נכון - בקומפוננטה
function MyComponent() {
    const [state, setState] = useState(0);
}

// ✅ נכון - ב-Custom Hook
function useMyHook() {
    const [state, setState] = useState(0);
    return state;
}

// ❌ שגוי - בפונקציה רגילה
function regularFunction() {
    const [state, setState] = useState(0); // אסור!
}

🔵 useState

כבר הכרנו את ה-Hook הזה בשיעור על State. הנה תזכורת קצרה:

const [value, setValue] = useState(initialValue);

// דוגמאות
const [count, setCount] = useState(0);
const [name, setName] = useState("");
const [items, setItems] = useState([]);
const [user, setUser] = useState(null);

🟢 useEffect

Hook זה מאפשר לסנכרן את הקומפוננטה עם העולם שמחוץ ל-React - פעולות שנקראות side effects (תופעות לוואי). בין השימושים הנפוצים:

  • קריאות לשרת (API)
  • הרשמה לאירועים והאזנה להם (Subscriptions)
  • שינוי ידני של ה-DOM
  • טיימרים ומרווחי זמן

מערך התלויות ([dependencies]) הוא הלב של ה-Hook: הוא קובע מתי האפקט ירוץ מחדש. פונקציית ה-cleanup שמחזירים מנקה אחריו - למשל מבטלת טיימר או מסירה מאזין.

התחביר

useEffect(() => {
    // הקוד שירוץ

    return () => {
        // cleanup (אופציונלי)
    };
}, [dependencies]); // מערך תלויות

ריצה בכל רינדור

useEffect(() => {
    console.log('רץ בכל רינדור');
});
// ללא מערך תלויות!

ריצה פעם אחת (mount)

useEffect(() => {
    console.log('רץ רק פעם אחת בטעינה');
    fetchData();
}, []); // מערך ריק!

ריצה כשתלויות משתנות

useEffect(() => {
    console.log('count השתנה:', count);
}, [count]); // רץ כש-count משתנה

Cleanup

useEffect(() => {
    const interval = setInterval(() => {
        console.log('טיק');
    }, 1000);

    // Cleanup - רץ לפני unmount או לפני הריצה הבאה
    return () => {
        clearInterval(interval);
    };
}, []);

דוגמה: Fetch Data

function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        setLoading(true);
        fetch(`/api/users/${userId}`)
            .then(res => res.json())
            .then(data => {
                setUser(data);
                setLoading(false);
            })
            .catch(err => {
                setError(err.message);
                setLoading(false);
            });
    }, [userId]); // רץ מחדש כשה-userId משתנה

    if (loading) return <p>טוען...</p>;
    if (error) return <p>שגיאה: {error}</p>;
    return <h1>{user.name}</h1>;
}

🟡 useRef

ל-useRef שני שימושים עיקריים: לגשת ישירות לאלמנט DOM (למשל כדי למקד שדה קלט), ולשמור ערך שנשמר בין רינדורים אך אינו גורם לרינדור מחדש כשהוא משתנה. זהו ההבדל המרכזי מ-State.

Reference ל-DOM

function TextInput() {
    const inputRef = useRef(null);

    const focusInput = () => {
        inputRef.current.focus();
    };

    return (
        <div>
            <input ref={inputRef} type="text" />
            <button onClick={focusInput}>Focus</button>
        </div>
    );
}

שמירת ערך (לא גורם לרינדור)

function Timer() {
    const [count, setCount] = useState(0);
    const intervalRef = useRef(null);

    const start = () => {
        intervalRef.current = setInterval(() => {
            setCount(c => c + 1);
        }, 1000);
    };

    const stop = () => {
        clearInterval(intervalRef.current);
    };

    return (
        <div>
            <p>{count}</p>
            <button onClick={start}>התחל</button>
            <button onClick={stop}>עצור</button>
        </div>
    );
}

🟣 useMemo

useMemo מיישם Memoization: הוא שומר את התוצאה של חישוב "כבד" ומריץ אותו מחדש רק כשאחת התלויות משתנה. כך נמנעים מחישובים מיותרים בכל רינדור.

function ExpensiveComponent({ items, filter }) {
    // מחושב מחדש רק כש-items או filter משתנים
    const filteredItems = useMemo(() => {
        console.log('מחשב...');
        return items.filter(item => item.name.includes(filter));
    }, [items, filter]);

    return (
        <ul>
            {filteredItems.map(item => (
                <li key={item.id}>{item.name}</li>
            ))}
        </ul>
    );
}

🔴 useCallback

בדומה ל-useMemo, אך עבור פונקציות: useCallback שומר את אותה פונקציה בין רינדורים במקום ליצור אותה מחדש בכל פעם. הוא שימושי במיוחד כשמעבירים callback לקומפוננטת ילד עטופה ב-React.memo, כדי למנוע רינדור מיותר שלה.

function Parent() {
    const [count, setCount] = useState(0);

    // הפונקציה לא נוצרת מחדש בכל רינדור
    const handleClick = useCallback(() => {
        console.log('נלחץ');
    }, []); // תלויות ריקות = לא משתנה לעולם

    return <Child onClick={handleClick} />;
}

const Child = React.memo(({ onClick }) => {
    console.log('Child rendered');
    return <button onClick={onClick}>לחץ</button>;
});

🟠 useContext

מאפשר לשתף נתונים בין קומפוננטות מרוחקות בעץ מבלי להעביר props דרך כל רמת ביניים (Props Drilling). זו הצצה ראשונה ל-Context API, שנרחיב עליו בשיעור נפרד.

import { createContext, useContext, useState } from 'react';

// יצירת Context
const ThemeContext = createContext();

// Provider - עוטף את האפליקציה
function App() {
    const [theme, setTheme] = useState('light');

    return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
            <Header />
            <Main />
        </ThemeContext.Provider>
    );
}

// צריכת ה-Context בכל קומפוננטה
function Header() {
    const { theme, setTheme } = useContext(ThemeContext);

    return (
        <header className={theme}>
            <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
                החלף ערכת נושא
            </button>
        </header>
    );
}

🎨 Custom Hooks

מעבר ל-Hooks המובנים, אפשר לכתוב Hooks משלנו. Custom Hook הוא פונקציה ששמה מתחיל ב-use ומרכזת בתוכה לוגיקה מבוססת-Hooks, כך שאפשר לעשות בה שימוש חוזר בכמה קומפוננטות בלי לשכפל קוד:

useLocalStorage

function useLocalStorage(key, initialValue) {
    const [value, setValue] = useState(() => {
        const saved = localStorage.getItem(key);
        return saved ? JSON.parse(saved) : initialValue;
    });

    useEffect(() => {
        localStorage.setItem(key, JSON.stringify(value));
    }, [key, value]);

    return [value, setValue];
}

// שימוש
function App() {
    const [name, setName] = useLocalStorage('name', '');
    // ...
}

useWindowSize

function useWindowSize() {
    const [size, setSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight
    });

    useEffect(() => {
        const handleResize = () => {
            setSize({
                width: window.innerWidth,
                height: window.innerHeight
            });
        };

        window.addEventListener('resize', handleResize);
        return () => window.removeEventListener('resize', handleResize);
    }, []);

    return size;
}

// שימוש
function Component() {
    const { width, height } = useWindowSize();
    return <p>גודל חלון: {width}x{height}</p>;
}

📋 סיכום Hooks

Hook שימוש
useState ניהול state
useEffect side effects (API, timers)
useRef reference ל-DOM או ערך קבוע
useMemo memoization של ערך מחושב
useCallback memoization של פונקציה
useContext שיתוף נתונים גלובלי

הקודם: ← אירועים
הבא: רינדור מותנה →

🔀 רינדור מותנה (Conditional Rendering)

מבוא

לעיתים קרובות נרצה להציג תוכן שונה בהתאם למצב - למשל הודעת טעינה בזמן שהנתונים נטענים, או תפריט אחר למשתמש מחובר. ב-React אין תחביר תנאי מיוחד: משתמשים ב-JavaScript רגיל בתוך ה-JSX. בשיעור זה נעבור על השיטות המקובלות ונראה מתי כל אחת מהן מתאימה.


📌 שיטות לרינדור מותנה

1. אופרטור Ternary ( ? : )

function Greeting({ isLoggedIn }) {
    return (
        <div>
            {isLoggedIn ? (
                <h1>ברוך הבא בחזרה!</h1>
            ) : (
                <h1>אנא התחבר</h1>
            )}
        </div>
    );
}

2. אופרטור && (Logical AND)

מציג תוכן רק אם התנאי true:

function Notifications({ count }) {
    return (
        <div>
            {count > 0 && (
                <span className="badge">{count}</span>
            )}
        </div>
    );
}

// אם count = 0, לא יוצג כלום
// אם count = 5, יוצג <span class="badge">5</span>

⚠️ זהירות למלכודת נפוצה: האופרטור && מחזיר את הצד השמאלי כשהוא "נפילה" (falsy). המספר 0 הוא falsy, אך React דווקא מציגה אותו על המסך. לכן בודקים תמיד תנאי מפורש (למשל count > 0) ולא נשענים על הערך עצמו:

// ❌ בעיה
{count && <span>{count}</span>}  // אם count=0, יוצג "0"

// ✅ פתרון
{count > 0 && <span>{count}</span>}

3. if/else רגיל

function StatusMessage({ status }) {
    let message;

    if (status === 'loading') {
        message = <p>טוען...</p>;
    } else if (status === 'error') {
        message = <p className="error">שגיאה!</p>;
    } else if (status === 'success') {
        message = <p className="success">הצלחה!</p>;
    } else {
        message = <p>לא ידוע</p>;
    }

    return <div>{message}</div>;
}

4. switch/case

function Icon({ type }) {
    switch (type) {
        case 'success':
            return <span>✅</span>;
        case 'error':
            return <span>❌</span>;
        case 'warning':
            return <span>⚠️</span>;
        default:
            return <span>ℹ️</span>;
    }
}

5. אובייקט מיפוי

function StatusIcon({ status }) {
    const icons = {
        success: '✅',
        error: '❌',
        warning: '⚠️',
        info: 'ℹ️'
    };

    return <span>{icons[status] || icons.info}</span>;
}

🎯 דוגמאות מעשיות

הצגת Loading

function UserList() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        fetchUsers().then(data => {
            setUsers(data);
            setLoading(false);
        });
    }, []);

    if (loading) {
        return <div className="spinner">טוען...</div>;
    }

    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
}

טיפול בשגיאות

function DataDisplay() {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        fetch('/api/data')
            .then(res => res.json())
            .then(setData)
            .catch(err => setError(err.message))
            .finally(() => setLoading(false));
    }, []);

    if (loading) return <p>⏳ טוען...</p>;
    if (error) return <p>❌ שגיאה: {error}</p>;
    if (!data) return <p>📭 אין נתונים</p>;

    return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

מצב ריק

function TodoList({ todos }) {
    if (todos.length === 0) {
        return (
            <div className="empty-state">
                <p>🎉 אין משימות!</p>
                <p>הוסף משימה חדשה להתחיל</p>
            </div>
        );
    }

    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id}>{todo.text}</li>
            ))}
        </ul>
    );
}

🙈 הסתרת קומפוננטה

החזרת null

function Modal({ isOpen, children }) {
    if (!isOpen) {
        return null; // לא מרנדר כלום
    }

    return (
        <div className="modal-overlay">
            <div className="modal-content">
                {children}
            </div>
        </div>
    );
}

CSS Display

function Toggle() {
    const [visible, setVisible] = useState(true);

    return (
        <div>
            <button onClick={() => setVisible(v => !v)}>
                {visible ? 'הסתר' : 'הצג'}
            </button>
            <div style={{ display: visible ? 'block' : 'none' }}>
                תוכן שאפשר להסתיר
            </div>
        </div>
    );
}

🎨 Classes דינמיים

function Button({ isActive, isDisabled, children }) {
    // שיטה 1: Template literal
    const className = `btn ${isActive ? 'active' : ''} ${isDisabled ? 'disabled' : ''}`;

    // שיטה 2: Array + filter + join
    const classNames = [
        'btn',
        isActive && 'active',
        isDisabled && 'disabled'
    ].filter(Boolean).join(' ');

    return (
        <button className={classNames} disabled={isDisabled}>
            {children}
        </button>
    );
}

📋 סיכום

שיטה מתי להשתמש
condition ? a : b שני מצבים אפשריים
condition && element הצגה/הסתרה פשוטה
if/else לוגיקה מורכבת
switch מספר מצבים קבועים
return null הסתרה מלאה

הקודם: ← Hooks
הבא: רשימות →

📋 רשימות (Lists)

מבוא

כמעט כל ממשק מציג נתונים בצורת רשימה - מוצרים, הודעות, משתמשים ועוד. ב-React ממירים מערך נתונים לרשימת אלמנטים בעזרת המתודה map(), שיוצרת אלמנט JSX עבור כל פריט במערך:

const fruits = ['תפוח', 'בננה', 'תפוז'];

function FruitList() {
    return (
        <ul>
            {fruits.map(fruit => (
                <li key={fruit}>{fruit}</li>
            ))}
        </ul>
    );
}

🔑 חשיבות ה-key

כל פריט ברשימה חייב key - מזהה ייחודי ויציב שעוזר ל-React לעקוב אחר הפריטים בין רינדורים ולזהות איזה מהם השתנה, נוסף או נמחק. בזכות ה-key, React מעדכנת רק את מה שהשתנה במקום לבנות את כל הרשימה מחדש. המזהה הטבעי ביותר הוא בדרך כלל ה-id שמגיע מהנתונים:

✅ נכון - key ייחודי

const users = [
    { id: 1, name: 'דני' },
    { id: 2, name: 'רונית' },
    { id: 3, name: 'משה' }
];

function UserList() {
    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
}

❌ בעייתי - index כ-key

// לא מומלץ! עלול לגרום לבאגים כשהרשימה משתנה
{items.map((item, index) => (
    <li key={index}>{item}</li>
))}

כללים ל-key

  1. חייב להיות ייחודי בין אחים
  2. חייב להיות יציב - לא להשתנות בין רינדורים
  3. לא להשתמש ב-Math.random() או Date.now()
  4. index מתאים רק לרשימות סטטיות שלא משתנות

📦 רינדור רשימות

רשימה פשוטה

function ColorList() {
    const colors = ['אדום', 'ירוק', 'כחול', 'צהוב'];

    return (
        <ul>
            {colors.map((color, index) => (
                <li key={index} style={{ color }}>
                    {color}
                </li>
            ))}
        </ul>
    );
}

רשימת אובייקטים

function ProductList() {
    const products = [
        { id: 1, name: 'מקלדת', price: 150 },
        { id: 2, name: 'עכבר', price: 80 },
        { id: 3, name: 'מסך', price: 1200 }
    ];

    return (
        <div className="products">
            {products.map(product => (
                <div key={product.id} className="product-card">
                    <h3>{product.name}</h3>
                    <p>מחיר: ₪{product.price}</p>
                </div>
            ))}
        </div>
    );
}

רשימה עם קומפוננטה

function ProductCard({ product }) {
    return (
        <div className="product-card">
            <h3>{product.name}</h3>
            <p>מחיר: ₪{product.price}</p>
            <button>הוסף לסל</button>
        </div>
    );
}

function ProductList({ products }) {
    return (
        <div className="products">
            {products.map(product => (
                <ProductCard key={product.id} product={product} />
            ))}
        </div>
    );
}

🔄 פעולות על רשימות

סינון (Filter)

function FilteredList() {
    const [filter, setFilter] = useState('');
    const items = ['תפוח', 'בננה', 'תפוז', 'אבטיח'];

    const filteredItems = items.filter(item =>
        item.includes(filter)
    );

    return (
        <div>
            <input 
                value={filter}
                onChange={(e) => setFilter(e.target.value)}
                placeholder="חפש..."
            />
            <ul>
                {filteredItems.map(item => (
                    <li key={item}>{item}</li>
                ))}
            </ul>
        </div>
    );
}

מיון (Sort)

function SortedList() {
    const [sortOrder, setSortOrder] = useState('asc');
    const items = [
        { id: 1, name: 'ב' },
        { id: 2, name: 'א' },
        { id: 3, name: 'ג' }
    ];

    const sortedItems = [...items].sort((a, b) => {
        if (sortOrder === 'asc') {
            return a.name.localeCompare(b.name, 'he');
        }
        return b.name.localeCompare(a.name, 'he');
    });

    return (
        <div>
            <button onClick={() => setSortOrder(o => o === 'asc' ? 'desc' : 'asc')}>
                מיון: {sortOrder === 'asc' ? '⬆️' : '⬇️'}
            </button>
            <ul>
                {sortedItems.map(item => (
                    <li key={item.id}>{item.name}</li>
                ))}
            </ul>
        </div>
    );
}

➕ הוספה ומחיקה

function TodoList() {
    const [todos, setTodos] = useState([
        { id: 1, text: 'לקנות חלב' },
        { id: 2, text: 'לשלם חשבונות' }
    ]);
    const [newTodo, setNewTodo] = useState('');

    // הוספה
    const addTodo = () => {
        if (!newTodo.trim()) return;
        setTodos(prev => [
            ...prev,
            { id: Date.now(), text: newTodo }
        ]);
        setNewTodo('');
    };

    // מחיקה
    const deleteTodo = (id) => {
        setTodos(prev => prev.filter(todo => todo.id !== id));
    };

    return (
        <div>
            <div>
                <input 
                    value={newTodo}
                    onChange={(e) => setNewTodo(e.target.value)}
                    onKeyPress={(e) => e.key === 'Enter' && addTodo()}
                />
                <button onClick={addTodo}>הוסף</button>
            </div>
            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        {todo.text}
                        <button onClick={() => deleteTodo(todo.id)}>❌</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

📊 טבלאות

function UserTable() {
    const users = [
        { id: 1, name: 'דני', age: 25, city: 'תל אביב' },
        { id: 2, name: 'רונית', age: 30, city: 'ירושלים' },
        { id: 3, name: 'משה', age: 28, city: 'חיפה' }
    ];

    return (
        <table>
            <thead>
                <tr>
                    <th>שם</th>
                    <th>גיל</th>
                    <th>עיר</th>
                </tr>
            </thead>
            <tbody>
                {users.map(user => (
                    <tr key={user.id}>
                        <td>{user.name}</td>
                        <td>{user.age}</td>
                        <td>{user.city}</td>
                    </tr>
                ))}
            </tbody>
        </table>
    );
}

🔢 רשימות מקוננות

function CategoryList() {
    const categories = [
        {
            id: 1,
            name: 'פירות',
            items: ['תפוח', 'בננה', 'תפוז']
        },
        {
            id: 2,
            name: 'ירקות',
            items: ['מלפפון', 'עגבנייה', 'גזר']
        }
    ];

    return (
        <div>
            {categories.map(category => (
                <div key={category.id}>
                    <h3>{category.name}</h3>
                    <ul>
                        {category.items.map((item, index) => (
                            <li key={index}>{item}</li>
                        ))}
                    </ul>
                </div>
            ))}
        </div>
    );
}

📋 סיכום

פעולה מתודה
הצגת רשימה array.map()
סינון array.filter()
מיון [...array].sort()
הוספה [...array, newItem]
מחיקה array.filter(x => x.id !== id)
עדכון array.map(x => x.id === id ? {...x, ...update} : x)

הקודם: ← רינדור מותנה
הבא: תרגילים →

📝 תרגילים

תרגילים לתרגול כל הנושאים

הגיע הזמן להפוך את החומר לידע מעשי. התרגילים מסודרים לפי רמת קושי - מבסיסי ועד מתקדם. מומלץ לנסות לפתור כל תרגיל באופן עצמאי לפני שמציצים ברמז, ולבדוק את הפתרון מול הדוגמאות בתיקיית examples.


🟢 רמה בסיסית

תרגיל 1: קומפוננטת ברכה

צרו קומפוננטה Greeting שמקבלת prop בשם name ומציגה "שלום, [name]!"

// התוצאה הרצויה:
<Greeting name="דני" />  // מציג: שלום, דני!
💡 רמז השתמשו ב-function component עם props destructuring.

תרגיל 2: מונה לחיצות

צרו קומפוננטה Counter עם כפתור שסופר לחיצות.

// פונקציונליות:
// - מציג את מספר הלחיצות
// - כפתור "הוסף" שמגדיל ב-1
// - כפתור "אפס" שמחזיר ל-0
💡 רמז השתמשו ב-useState לניהול המונה.

תרגיל 3: Toggle

צרו קומפוננטה שמחליפה בין "פועל" ו"כבוי" בלחיצה על כפתור.

💡 רמז השתמשו ב-useState עם ערך בוליאני.

🟡 רמה בינונית

תרגיל 4: רשימת משימות

צרו אפליקציית רשימת משימות (Todo List) שכוללת:

  • שדה קלט להוספת משימה חדשה
  • רשימה של המשימות הקיימות
  • אפשרות למחוק משימה
  • אפשרות לסמן משימה כ"בוצעה"
💡 רמז
const [todos, setTodos] = useState([]);
// כל todo: { id, text, completed }

תרגיל 5: טופס הרשמה

צרו טופס הרשמה שכולל:

  • שדה שם (חובה, לפחות 2 תווים)
  • שדה אימייל (חובה, בפורמט תקין)
  • שדה סיסמה (חובה, לפחות 6 תווים)
  • הודעות שגיאה מתאימות לכל שדה
  • כפתור שליחה שפעיל רק כשכל השדות תקינים
💡 רמז השתמשו ב-state לכל שדה + state לשגיאות.

תרגיל 6: Timer

צרו טיימר שכולל:

  • תצוגת זמן בפורמט דקות:שניות
  • כפתור "התחל"
  • כפתור "עצור"
  • כפתור "אפס"
💡 רמז השתמשו ב-useEffect עם setInterval, ושמרו את ה-interval ב-useRef.

🔴 רמה מתקדמת

תרגיל 7: Fetch Data

צרו קומפוננטה שמביאה נתונים מ-API. הדרישות:

  • שליפת נתונים מהכתובת https://jsonplaceholder.typicode.com/users
  • הצגת מצב טעינה (loading) בזמן השליפה
  • הצגת הודעת שגיאה אם השליפה נכשלה
  • הצגת רשימת המשתמשים בסיום
💡 רמז
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
    fetch(url)
        .then(res => res.json())
        .then(data => setUsers(data))
        .catch(err => setError(err.message))
        .finally(() => setLoading(false));
}, []);

תרגיל 8: Theme Context

צרו מערכת ערכות נושא (Theme) המבוססת על Context:

  • שתי ערכות נושא: בהירה וכהה
  • כפתור להחלפה ביניהן
  • כל הקומפוננטות מתעדכנות בהתאם לערכה הפעילה
💡 רמז
const ThemeContext = createContext();

function ThemeProvider({ children }) {
    const [theme, setTheme] = useState('light');
    // ...
}

תרגיל 9: חנות (Mini E-Commerce)

צרו חנות מיני שכוללת:

  • רשימת מוצרים (שם, מחיר ותמונה)
  • אפשרות להוסיף מוצר לעגלה
  • תצוגת עגלה עם סכום כולל לתשלום
  • אפשרות להסיר מוצר מהעגלה

תרגיל 10: Custom Hook - useLocalStorage

צרו Custom Hook שעובד כמו useState אבל שומר את הנתונים ב-localStorage:

const [name, setName] = useLocalStorage('user-name', '');
// הערך נשמר גם אחרי רענון הדף!

📌 טיפים לפתרון

  1. התחילו פשוט - קודם גרמו לזה לעבוד, אחר כך שפרו
  2. חלקו לקומפוננטות - כל חלק נפרד בקומפוננטה משלו
  3. בדקו תוך כדי - הריצו ובדקו אחרי כל שינוי קטן
  4. קראו את השגיאות - הן מספרות בדיוק מה הבעיה

✅ פתרונות

הפתרונות נמצאים בתיקיית examples:

  • תרגילים 1-3: 01_hello_component.jsx
  • תרגיל 4: 08_todo_app.jsx
  • תרגיל 5: 04_events_example.jsx
  • תרגיל 6: 05_hooks_example.jsx

בהצלחה! 🎉


הקודם: ← רשימות
הבא: Lifecycle & useEffect →

🔄 React Lifecycle & useEffect

מבוא

Lifecycle (מחזור חיים) הוא שם כולל לשלבים שכל קומפוננטה עוברת מרגע הופעתה על המסך ועד הסרתה: יצירה (Mount), עדכון (Update) והריסה (Unmount). הבנת המחזור הזה חיונית כדי לדעת מתי לטעון נתונים, מתי להגיב לשינויים ומתי לשחרר משאבים.

בקומפוננטות פונקציונליות מנהלים את כל השלבים האלה באמצעות Hook יחיד - useEffect - שבו נתמקד בשיעור הזה.


📌 מהו useEffect?

useEffect הוא Hook שמאפשר לבצע side effects - פעולות שמסנכרנות את הקומפוננטה עם משהו שנמצא מחוץ לה. בין השימושים הנפוצים:

  • קריאות לשרת (API)
  • הרשמה לאירועים והאזנה להם (subscriptions)
  • שינויים ידניים ב-DOM
  • טיימרים ומרווחי זמן

התחביר הבסיסי

import { useEffect } from 'react';

useEffect(() => {
    // הקוד שירוץ (effect)

    return () => {
        // cleanup (אופציונלי) - רץ לפני הריסה או לפני האפקט הבא
    };
}, [dependencies]); // מערך תלויות

🎯 מערך התלויות (Dependencies Array)

מערך התלויות קובע מתי האפקט ירוץ:

1. ללא מערך - רץ בכל רינדור

useEffect(() => {
    console.log('רץ בכל רינדור!');
});
// ⚠️ זהירות! עלול לגרום לבעיות ביצועים

2. מערך ריק [] - רץ פעם אחת (Mount)

useEffect(() => {
    console.log('רץ רק בטעינה הראשונה');

    // שווה ערך ל-componentDidMount
}, []);

3. עם תלויות - רץ כשהתלות משתנה

useEffect(() => {
    console.log(`הערך השתנה ל: ${value}`);

    // שווה ערך ל-componentDidUpdate (עבור value)
}, [value]);

4. מספר תלויות

useEffect(() => {
    console.log('אחד מהערכים השתנה');
}, [value1, value2, value3]);

🔄 השוואה ל-Class Lifecycle

Class Component Function Component + useEffect
componentDidMount useEffect(() => {}, [])
componentDidUpdate useEffect(() => {}, [deps])
componentWillUnmount useEffect(() => { return () => {} }, [])

דוגמה מפורטת

// Class Component (סגנון ישן)
class Example extends React.Component {
    componentDidMount() {
        console.log('נטען');
        document.title = 'נטען';
    }

    componentDidUpdate(prevProps) {
        if (prevProps.count !== this.props.count) {
            console.log('count השתנה');
        }
    }

    componentWillUnmount() {
        console.log('נהרס');
    }
}

// Function Component (סגנון מודרני)
function Example({ count }) {
    // componentDidMount
    useEffect(() => {
        console.log('נטען');
        document.title = 'נטען';
    }, []);

    // componentDidUpdate for count
    useEffect(() => {
        console.log('count השתנה');
    }, [count]);

    // componentWillUnmount
    useEffect(() => {
        return () => {
            console.log('נהרס');
        };
    }, []);
}

🧹 Cleanup Function

פונקציית ה-cleanup היא פונקציה שמחזירים מתוך האפקט, ותפקידה לנקות אחריו. היא רצה בשני מצבים:

  1. לפני הריסת הקומפוננטה (Unmount)
  2. לפני כל הרצה חוזרת של האפקט, כדי לבטל את ההרצה הקודמת

למה צריך Cleanup?

// ❌ בעיה: דליפת זיכרון (Memory Leak)
function Timer() {
    const [seconds, setSeconds] = useState(0);

    useEffect(() => {
        const interval = setInterval(() => {
            setSeconds(s => s + 1);
        }, 1000);
        // הטיימר ממשיך לרוץ גם אחרי שהקומפוננטה נהרסת!
    }, []);

    return <p>{seconds}</p>;
}

// ✅ פתרון: Cleanup
function Timer() {
    const [seconds, setSeconds] = useState(0);

    useEffect(() => {
        const interval = setInterval(() => {
            setSeconds(s => s + 1);
        }, 1000);

        // Cleanup - עוצר את הטיימר
        return () => {
            clearInterval(interval);
        };
    }, []);

    return <p>{seconds}</p>;
}

דוגמאות נוספות ל-Cleanup

// Event Listeners
useEffect(() => {
    const handleResize = () => console.log(window.innerWidth);
    window.addEventListener('resize', handleResize);

    return () => {
        window.removeEventListener('resize', handleResize);
    };
}, []);

// WebSocket
useEffect(() => {
    const socket = new WebSocket('wss://example.com');
    socket.onmessage = (event) => console.log(event.data);

    return () => {
        socket.close();
    };
}, []);

// AbortController (לביטול fetch)
useEffect(() => {
    const controller = new AbortController();

    fetch('/api/data', { signal: controller.signal })
        .then(res => res.json())
        .then(setData)
        .catch(err => {
            if (err.name !== 'AbortError') {
                setError(err.message);
            }
        });

    return () => {
        controller.abort();
    };
}, []);

⚠️ Memory Leaks - דליפות זיכרון

דליפת זיכרון (Memory Leak) מתרחשת כשהקוד ממשיך לרוץ או להחזיק במשאבים גם אחרי שהקומפוננטה כבר נהרסה. הסיבות השכיחות:

  • טיימרים שלא נעצרים
  • מאזיני אירועים (Event listeners) שלא מוסרים
  • קריאות API שלא מבוטלות
  • הרשמות (Subscriptions) שלא מנותקות

סימנים לדליפת זיכרון

Warning: Can't perform a React state update on an unmounted component.

מניעת דליפות

function DataFetcher({ url }) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        let isMounted = true; // דגל לבדיקה

        async function fetchData() {
            try {
                const res = await fetch(url);
                const json = await res.json();

                // עדכון רק אם הקומפוננטה עדיין קיימת
                if (isMounted) {
                    setData(json);
                    setLoading(false);
                }
            } catch (error) {
                if (isMounted) {
                    setLoading(false);
                }
            }
        }

        fetchData();

        return () => {
            isMounted = false; // סימון שהקומפוננטה נהרסה
        };
    }, [url]);

    if (loading) return <p>טוען...</p>;
    return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

📝 דפוסים נפוצים

1. Fetch בטעינה

useEffect(() => {
    fetch('/api/users')
        .then(res => res.json())
        .then(setUsers);
}, []);

2. עדכון כותרת הדף

useEffect(() => {
    document.title = `${count} הודעות חדשות`;
}, [count]);

3. הרשמה לאירועי חלון

useEffect(() => {
    const handleScroll = () => setScrollY(window.scrollY);
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
}, []);

4. Debounce בחיפוש

useEffect(() => {
    const timeoutId = setTimeout(() => {
        if (searchTerm) {
            searchAPI(searchTerm).then(setResults);
        }
    }, 300);

    return () => clearTimeout(timeoutId);
}, [searchTerm]);

5. סנכרון עם localStorage

useEffect(() => {
    localStorage.setItem('theme', theme);
}, [theme]);

❌ טעויות נפוצות

1. שכחת תלויות

// ❌ שגוי - count לא במערך התלויות
useEffect(() => {
    console.log(count); // תמיד יהיה הערך ההתחלתי
}, []);

// ✅ נכון
useEffect(() => {
    console.log(count);
}, [count]);

2. לולאה אינסופית

// ❌ שגוי - גורם ללולאה אינסופית
useEffect(() => {
    setData([...data, newItem]); // data משתנה → אפקט רץ שוב → ...
}, [data]);

// ✅ נכון - שימוש ב-callback
useEffect(() => {
    setData(prev => [...prev, newItem]);
}, [newItem]);

3. אובייקט/מערך כתלות

// ❌ בעייתי - אובייקט חדש בכל רינדור
const options = { page: 1, limit: 10 };
useEffect(() => {
    fetch('/api', options);
}, [options]); // רץ בכל רינדור!

// ✅ פתרון 1: useMemo
const options = useMemo(() => ({ page, limit }), [page, limit]);

// ✅ פתרון 2: תלויות פרימיטיביות
useEffect(() => {
    fetch('/api', { page, limit });
}, [page, limit]);

📋 סיכום

מצב תחביר
Mount (פעם אחת) useEffect(() => {}, [])
Update (כשמשתנה) useEffect(() => {}, [dep])
Unmount (cleanup) useEffect(() => () => {}, [])
כל רינדור useEffect(() => {})

הקודם: ← תרגילים
הבא: Props Drilling & Lifting State →

🔗 Props Drilling & Lifting State Up

מבוא

ככל שאפליקציה גדלה ועץ הקומפוננטות מסתעף, צצות שתי בעיות נפוצות בניהול הנתונים:

  1. Props Drilling - הצורך להעביר props דרך רמות ביניים רבות שאינן זקוקות להם
  2. שיתוף State - מצב שבו כמה קומפוננטות צריכות לקרוא ולעדכן את אותו נתון

בשיעור זה נכיר את שתי הבעיות ואת הפתרון הבסיסי לשיתוף State - Lifting State Up.


📌 Props Drilling - הבעיה

Props Drilling קורה כשמעבירים props דרך קומפוננטות ביניים שלא צריכות אותם:

// ❌ Props Drilling - הנתון עובר דרך 3 רמות!
function App() {
    const [user, setUser] = useState({ name: 'דני' });

    return <Layout user={user} />;  // רמה 1
}

function Layout({ user }) {
    return (
        <div>
            <Sidebar user={user} />  {/* רמה 2 - לא צריך את user */}
            <Content />
        </div>
    );
}

function Sidebar({ user }) {
    return (
        <div>
            <UserProfile user={user} />  {/* רמה 3 - לא צריך את user */}
        </div>
    );
}

function UserProfile({ user }) {
    return <p>שלום, {user.name}!</p>;  // רמה 4 - כאן באמת צריך!
}

בעיות של Props Drilling:

  1. קוד מסורבל וקשה לתחזוקה
  2. קומפוננטות ביניים "מזוהמות" ב-props לא רלוונטיים
  3. קשה לשנות את מבנה הנתונים

🔝 Lifting State Up - פתרון לשיתוף State

העיקרון של Lifting State Up ("הרמת המצב") פשוט: כאשר כמה קומפוננטות צריכות לשתף נתון, מעבירים אותו למעלה - אל קומפוננטת ההורה המשותפת הקרובה ביותר. ההורה מחזיק את ה-State ומעביר אותו לילדים כ-props, יחד עם פונקציית עדכון. כך כל הילדים עובדים על אותו מקור נתונים יחיד ונשארים מסונכרנים.

הבעיה: שתי קומפוננטות צריכות את אותו נתון

// ❌ בעיה - כל קומפוננטה עם state נפרד
function TemperatureInput() {
    const [celsius, setCelsius] = useState(0);
    return <input value={celsius} onChange={e => setCelsius(e.target.value)} />;
}

function TemperatureDisplay() {
    const [celsius, setCelsius] = useState(0); // לא מסונכרן!
    return <p>{celsius}°C = {(celsius * 9/5) + 32}°F</p>;
}

הפתרון: Lifting State Up

// ✅ פתרון - State בהורה המשותף
function TemperatureConverter() {
    // State מורם להורה
    const [celsius, setCelsius] = useState(0);

    return (
        <div>
            <TemperatureInput 
                value={celsius} 
                onChange={setCelsius} 
            />
            <TemperatureDisplay celsius={celsius} />
        </div>
    );
}

function TemperatureInput({ value, onChange }) {
    return (
        <input 
            type="number"
            value={value} 
            onChange={e => onChange(Number(e.target.value))} 
        />
    );
}

function TemperatureDisplay({ celsius }) {
    const fahrenheit = (celsius * 9/5) + 32;
    return <p>{celsius}°C = {fahrenheit}°F</p>;
}

📝 דוגמה מלאה: טופס עם ולידציה

function RegistrationForm() {
    // State מורם - כל הנתונים בהורה
    const [formData, setFormData] = useState({
        email: '',
        password: '',
        confirmPassword: ''
    });

    const [errors, setErrors] = useState({});

    // פונקציית עדכון שמועברת לילדים
    const updateField = (field, value) => {
        setFormData(prev => ({ ...prev, [field]: value }));
    };

    // ולידציה
    const validate = () => {
        const newErrors = {};
        if (!formData.email.includes('@')) {
            newErrors.email = 'אימייל לא תקין';
        }
        if (formData.password.length < 6) {
            newErrors.password = 'סיסמה חייבת להכיל לפחות 6 תווים';
        }
        if (formData.password !== formData.confirmPassword) {
            newErrors.confirmPassword = 'הסיסמאות לא תואמות';
        }
        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
    };

    return (
        <form onSubmit={e => { e.preventDefault(); validate(); }}>
            <EmailInput 
                value={formData.email}
                onChange={v => updateField('email', v)}
                error={errors.email}
            />
            <PasswordInput 
                value={formData.password}
                onChange={v => updateField('password', v)}
                error={errors.password}
            />
            <ConfirmPasswordInput 
                value={formData.confirmPassword}
                onChange={v => updateField('confirmPassword', v)}
                error={errors.confirmPassword}
            />
            <FormSummary formData={formData} errors={errors} />
            <button type="submit">הרשם</button>
        </form>
    );
}

function EmailInput({ value, onChange, error }) {
    return (
        <div>
            <input 
                type="email"
                value={value}
                onChange={e => onChange(e.target.value)}
                placeholder="אימייל"
            />
            {error && <span className="error">{error}</span>}
        </div>
    );
}

function PasswordInput({ value, onChange, error }) {
    return (
        <div>
            <input 
                type="password"
                value={value}
                onChange={e => onChange(e.target.value)}
                placeholder="סיסמה"
            />
            {error && <span className="error">{error}</span>}
        </div>
    );
}

function ConfirmPasswordInput({ value, onChange, error }) {
    return (
        <div>
            <input 
                type="password"
                value={value}
                onChange={e => onChange(e.target.value)}
                placeholder="אימות סיסמה"
            />
            {error && <span className="error">{error}</span>}
        </div>
    );
}

function FormSummary({ formData, errors }) {
    const isValid = Object.keys(errors).length === 0 && 
                    formData.email && formData.password;

    return (
        <div className="summary">
            <p>סטטוס: {isValid ? '✅ תקין' : '❌ לא מלא'}</p>
        </div>
    );
}

🔄 דפוס: Controlled Component

קומפוננטה מבוקרת (Controlled Component) היא קומפוננטה שאינה מחזיקה State משלה, אלא מקבלת את הערך שלה מההורה ומדווחת לו על כל שינוי. זהו יישום ישיר של Lifting State Up, והדפוס המקובל לעבודה עם שדות טופס ב-React:

// ✅ Controlled Component
function TextInput({ value, onChange, label }) {
    return (
        <label>
            {label}:
            <input 
                value={value}           // ערך מההורה
                onChange={e => onChange(e.target.value)}  // עדכון להורה
            />
        </label>
    );
}

// שימוש
function Parent() {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');

    return (
        <div>
            <TextInput label="שם" value={name} onChange={setName} />
            <TextInput label="אימייל" value={email} onChange={setEmail} />
        </div>
    );
}

📊 מתי להשתמש בכל גישה?

מצב פתרון מומלץ
2-3 קומפוננטות משתפות state Lifting State Up
Props עוברים דרך 3+ רמות Context API (פרק הבא)
State גלובלי מורכב Redux / Zustand
State מקומי בלבד useState רגיל

⚠️ טעויות נפוצות

1. הרמה לא נכונה

// ❌ שגוי - State ברמה גבוהה מדי
function App() {
    const [inputValue, setInputValue] = useState(''); // לא צריך להיות כאן!
    return <DeepNestedComponent value={inputValue} />;
}

// ✅ נכון - State בהורה המשותף הקרוב ביותר
function FormSection() {
    const [inputValue, setInputValue] = useState('');
    return <SomeInput value={inputValue} onChange={setInputValue} />;
}

2. שכפול State

// ❌ שגוי - State משוכפל
function Child({ user }) {
    const [localUser, setLocalUser] = useState(user); // שכפול!
    // localUser לא יתעדכן כשהprop משתנה
}

// ✅ נכון - שימוש ישיר ב-prop
function Child({ user, onUserChange }) {
    // ללא state מקומי, עובדים ישירות עם ה-prop
}

📋 סיכום

מושג הסבר
Props Drilling העברת props דרך קומפוננטות ביניים
Lifting State Up העלאת state להורה משותף
Controlled Component קומפוננטה ש-state שלה מנוהל בחוץ
Single Source of Truth מקור אחד לנתון

הקודם: ← Lifecycle & useEffect
הבא: Component Communication Patterns →

🔀 Component Communication Patterns

מבוא

אפליקציה אמיתית בנויה מעשרות קומפוננטות שצריכות להעביר מידע ביניהן. הדרך הנכונה לתקשר תלויה בקשר בין הקומפוננטות בעץ. אלו דפוסי התקשורת המרכזיים, מהפשוט למתקדם:

  1. Parent → Child: העברת נתונים כלפי מטה באמצעות Props
  2. Child → Parent: החזרת מידע כלפי מעלה באמצעות פונקציות Callback
  3. Siblings (אחים): תקשורת דרך ההורה המשותף (Lifting State Up)
  4. Deep / Global: שיתוף רחב באמצעות Context API או ספריית ניהול State

📌 1. Parent → Child (Props)

הדרך הפשוטה ביותר - העברת נתונים מהורה לילד:

// Parent מעביר נתונים ל-Child
function Parent() {
    const data = { name: 'דני', age: 25 };

    return <Child user={data} />;
}

function Child({ user }) {
    return <p>שלום {user.name}, בן {user.age}</p>;
}

📌 2. Child → Parent (Callbacks)

הילד קורא לפונקציה שנשלחה מההורה:

// Parent מקבל נתונים מ-Child
function Parent() {
    const [message, setMessage] = useState('');

    const handleMessage = (msg) => {
        console.log('התקבל מהילד:', msg);
        setMessage(msg);
    };

    return (
        <div>
            <Child onSendMessage={handleMessage} />
            <p>הודעה מהילד: {message}</p>
        </div>
    );
}

function Child({ onSendMessage }) {
    return (
        <button onClick={() => onSendMessage('שלום מהילד!')}>
            שלח הודעה להורה
        </button>
    );
}

דוגמה מלאה: טופס

function ProductForm() {
    const handleSubmit = (productData) => {
        console.log('מוצר חדש:', productData);
        // שליחה לשרת וכו'
    };

    return <ProductFormInputs onSubmit={handleSubmit} />;
}

function ProductFormInputs({ onSubmit }) {
    const [name, setName] = useState('');
    const [price, setPrice] = useState('');

    const handleFormSubmit = (e) => {
        e.preventDefault();
        onSubmit({ name, price: Number(price) });
    };

    return (
        <form onSubmit={handleFormSubmit}>
            <input 
                value={name} 
                onChange={e => setName(e.target.value)}
                placeholder="שם מוצר"
            />
            <input 
                type="number"
                value={price} 
                onChange={e => setPrice(e.target.value)}
                placeholder="מחיר"
            />
            <button type="submit">הוסף מוצר</button>
        </form>
    );
}

📌 3. Sibling Communication

קומפוננטות אחיות מתקשרות דרך ההורה המשותף:

function App() {
    const [selectedItem, setSelectedItem] = useState(null);

    return (
        <div className="app">
            {/* אח 1: בוחר פריט */}
            <ItemList 
                onSelect={setSelectedItem} 
            />

            {/* אח 2: מציג את הבחירה */}
            <ItemDetails 
                item={selectedItem} 
            />
        </div>
    );
}

function ItemList({ onSelect }) {
    const items = ['פריט א', 'פריט ב', 'פריט ג'];

    return (
        <ul>
            {items.map(item => (
                <li key={item} onClick={() => onSelect(item)}>
                    {item}
                </li>
            ))}
        </ul>
    );
}

function ItemDetails({ item }) {
    if (!item) return <p>בחר פריט מהרשימה</p>;
    return <p>נבחר: {item}</p>;
}

📌 4. Event Emitter Pattern

דפוס מתקדם המבוסס על "אפיק אירועים" מרכזי (Event Bus): קומפוננטה אחת משדרת אירוע והאחרות מאזינות לו, בלי קשר ישיר ביניהן. ברוב המקרים Context API יהיה פשוט ונקי יותר, אך כדאי להכיר את הדפוס:

// פשוט יותר להשתמש ב-Context, אבל זה דפוס אפשרי
const eventBus = {
    events: {},

    subscribe(event, callback) {
        if (!this.events[event]) {
            this.events[event] = [];
        }
        this.events[event].push(callback);

        // Unsubscribe function
        return () => {
            this.events[event] = this.events[event].filter(cb => cb !== callback);
        };
    },

    emit(event, data) {
        if (this.events[event]) {
            this.events[event].forEach(callback => callback(data));
        }
    }
};

function ComponentA() {
    return (
        <button onClick={() => eventBus.emit('message', 'שלום!')}>
            שלח הודעה
        </button>
    );
}

function ComponentB() {
    const [message, setMessage] = useState('');

    useEffect(() => {
        const unsubscribe = eventBus.subscribe('message', setMessage);
        return unsubscribe;
    }, []);

    return <p>הודעה: {message}</p>;
}

📌 5. Compound Components Pattern

דפוס שבו קבוצת קומפוננטות עובדת יחד כיחידה אחת וחולקת State פנימי, בעוד המשתמש מרכיב אותן בחופשיות. דוגמה קלאסית היא רכיב טאבים (Tabs ו-Tab), שמעניק API הצהרתי ונקי:

// קומפוננטות שמשתפות state באופן מובנה
function Tabs({ children, defaultTab = 0 }) {
    const [activeTab, setActiveTab] = useState(defaultTab);

    return (
        <div className="tabs">
            <div className="tab-list">
                {React.Children.map(children, (child, index) => (
                    <button 
                        className={activeTab === index ? 'active' : ''}
                        onClick={() => setActiveTab(index)}
                    >
                        {child.props.label}
                    </button>
                ))}
            </div>
            <div className="tab-content">
                {React.Children.toArray(children)[activeTab]}
            </div>
        </div>
    );
}

function Tab({ children, label }) {
    return <div>{children}</div>;
}

// שימוש
function App() {
    return (
        <Tabs defaultTab={0}>
            <Tab label="פרופיל">
                <h2>פרופיל משתמש</h2>
                <p>תוכן הפרופיל...</p>
            </Tab>
            <Tab label="הגדרות">
                <h2>הגדרות</h2>
                <p>תוכן ההגדרות...</p>
            </Tab>
            <Tab label="התראות">
                <h2>התראות</h2>
                <p>תוכן ההתראות...</p>
            </Tab>
        </Tabs>
    );
}

📌 6. Render Props Pattern

דפוס שבו קומפוננטה מקבלת פונקציית רינדור כ-prop, ומעבירה אליה את ה-State הפנימי שלה. כך אפשר לשתף לוגיקה (כמו מעקב אחר מיקום העכבר) בין קומפוננטות, בעוד כל אחת מהן מחליטה בעצמה כיצד להציג את התוצאה:

// קומפוננטה שחושפת state דרך render prop
function MouseTracker({ render }) {
    const [position, setPosition] = useState({ x: 0, y: 0 });

    useEffect(() => {
        const handleMouseMove = (e) => {
            setPosition({ x: e.clientX, y: e.clientY });
        };
        window.addEventListener('mousemove', handleMouseMove);
        return () => window.removeEventListener('mousemove', handleMouseMove);
    }, []);

    // קוראים לפונקציה שהתקבלה עם ה-state
    return render(position);
}

// שימוש
function App() {
    return (
        <MouseTracker 
            render={({ x, y }) => (
                <div>
                    <p>מיקום העכבר: {x}, {y}</p>
                    <div 
                        style={{
                            position: 'absolute',
                            left: x,
                            top: y,
                            width: 20,
                            height: 20,
                            background: 'red',
                            borderRadius: '50%'
                        }}
                    />
                </div>
            )}
        />
    );
}

📊 השוואה בין דפוסים

דפוס שימוש יתרונות חסרונות
Props Parent → Child פשוט, ברור Props drilling
Callbacks Child → Parent גמיש יכול להסתבך
Lifting State Siblings מקור אחד לאמת קוד בהורה
Context Deep/Global ללא drilling Rerenders
Compound קומפוננטות קשורות API נקי מורכב
Render Props לוגיקה משותפת גמיש מאוד Callback hell

📋 סיכום

// Parent → Child
<Child data={data} />

// Child → Parent
<Child onChange={(value) => setParentState(value)} />

// Siblings (דרך הורה)
<Brother1 onSelect={setShared} />
<Brother2 selected={shared} />

הקודם: ← Props Drilling
הבא: Context API →

🌐 Context API

מבוא

Context API הוא המנגנון המובנה של React לשיתוף נתונים "גלובליים" בתוך עץ הקומפוננטות, והפתרון הישיר לבעיית ה-Props Drilling. במקום להעביר prop יד ביד דרך כל רמת ביניים, מגדירים Context פעם אחת - וכל קומפוננטה בתוך העץ יכולה לגשת אליו ישירות. הוא מתאים במיוחד לנתונים שנצרכים במקומות רבים, כמו ערכת נושא, שפה או פרטי המשתמש המחובר.

ללא Context:          עם Context:
App ──► Layout         App (Provider)
    ──► Sidebar            ├── Layout
        ──► UserCard       ├── Sidebar
            ──► Avatar         └── Avatar (Consumer)
                                   ↑ נגיש ישירות!

📌 שלושת השלבים

העבודה עם Context בנויה תמיד משלושה שלבים: יוצרים את ה-Context, עוטפים את העץ ב-Provider שמספק את הערך, וצורכים את הערך בכל קומפוננטה שצריכה אותו.

1. Create - יצירת Context

import { createContext } from 'react';

// יצירת Context עם ערך ברירת מחדל
const ThemeContext = createContext('light');

// או בלי ערך ברירת מחדל
const UserContext = createContext(null);

2. Provide - הספקת הערך

import { ThemeContext } from './ThemeContext';

function App() {
    const [theme, setTheme] = useState('dark');

    return (
        // Provider עוטף את כל הקומפוננטות שצריכות גישה
        <ThemeContext.Provider value={theme}>
            <Header />
            <Main />
            <Footer />
        </ThemeContext.Provider>
    );
}

3. Consume - צריכת הערך

import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function ThemedButton() {
    // שימוש ב-useContext לקבלת הערך
    const theme = useContext(ThemeContext);

    return (
        <button className={`btn-${theme}`}>
            כפתור ב-{theme} mode
        </button>
    );
}

📝 דוגמה מלאה: Theme Context

קובץ ThemeContext.jsx

import { createContext, useContext, useState } from 'react';

// יצירת Context
const ThemeContext = createContext(null);

// Provider Component
export function ThemeProvider({ children }) {
    const [theme, setTheme] = useState('light');

    const toggleTheme = () => {
        setTheme(prev => prev === 'light' ? 'dark' : 'light');
    };

    // ערך ה-Context כולל את ה-state והפונקציות
    const value = {
        theme,
        toggleTheme,
        isDark: theme === 'dark'
    };

    return (
        <ThemeContext.Provider value={value}>
            {children}
        </ThemeContext.Provider>
    );
}

// Custom Hook לשימוש נוח
export function useTheme() {
    const context = useContext(ThemeContext);

    if (context === null) {
        throw new Error('useTheme must be used within ThemeProvider');
    }

    return context;
}

שימוש

// App.jsx
import { ThemeProvider } from './ThemeContext';

function App() {
    return (
        <ThemeProvider>
            <Header />
            <Main />
        </ThemeProvider>
    );
}

// Header.jsx
import { useTheme } from './ThemeContext';

function Header() {
    const { theme, toggleTheme } = useTheme();

    return (
        <header className={theme}>
            <button onClick={toggleTheme}>
                {theme === 'light' ? '🌙' : '☀️'}
            </button>
        </header>
    );
}

// ThemedCard.jsx
import { useTheme } from './ThemeContext';

function ThemedCard({ children }) {
    const { isDark } = useTheme();

    return (
        <div style={{
            backgroundColor: isDark ? '#333' : '#fff',
            color: isDark ? '#fff' : '#333',
            padding: '20px'
        }}>
            {children}
        </div>
    );
}

📝 דוגמה: Auth Context

import { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        // בדיקת משתמש מחובר בטעינה
        const savedUser = localStorage.getItem('user');
        if (savedUser) {
            setUser(JSON.parse(savedUser));
        }
        setLoading(false);
    }, []);

    const login = async (email, password) => {
        // קריאה ל-API
        const response = await fetch('/api/login', {
            method: 'POST',
            body: JSON.stringify({ email, password })
        });
        const userData = await response.json();

        setUser(userData);
        localStorage.setItem('user', JSON.stringify(userData));
    };

    const logout = () => {
        setUser(null);
        localStorage.removeItem('user');
    };

    const value = {
        user,
        isAuthenticated: !!user,
        loading,
        login,
        logout
    };

    return (
        <AuthContext.Provider value={value}>
            {children}
        </AuthContext.Provider>
    );
}

export function useAuth() {
    const context = useContext(AuthContext);
    if (!context) {
        throw new Error('useAuth must be used within AuthProvider');
    }
    return context;
}

// שימוש בקומפוננטות
function LoginButton() {
    const { isAuthenticated, logout } = useAuth();

    if (isAuthenticated) {
        return <button onClick={logout}>התנתק</button>;
    }
    return <a href="/login">התחבר</a>;
}

function ProtectedRoute({ children }) {
    const { isAuthenticated, loading } = useAuth();

    if (loading) return <p>טוען...</p>;
    if (!isAuthenticated) return <Navigate to="/login" />;

    return children;
}

🔄 שילוב מספר Contexts

function App() {
    return (
        <AuthProvider>
            <ThemeProvider>
                <LanguageProvider>
                    <Router>
                        <MainApp />
                    </Router>
                </LanguageProvider>
            </ThemeProvider>
        </AuthProvider>
    );
}

// קומפוננטה שמשתמשת במספר contexts
function Dashboard() {
    const { user } = useAuth();
    const { theme } = useTheme();
    const { language } = useLanguage();

    return (
        <div className={theme}>
            <h1>{language === 'he' ? 'שלום' : 'Hello'}, {user.name}</h1>
        </div>
    );
}

⚠️ Context Performance

חשוב להכיר את מחיר הביצועים: בכל פעם שערך ה-Context משתנה, כל הקומפוננטות שצורכות אותו מתרנדרות מחדש. לכן כדאי לפצל Context לפי תחומים ולייצב את הערך שמעבירים, כפי שנראה בהמשך:

בעיה

// ❌ כל שינוי ב-count גורם לרינדור של כל הצרכנים
function AppProvider({ children }) {
    const [user, setUser] = useState(null);
    const [count, setCount] = useState(0);

    return (
        <AppContext.Provider value={{ user, setUser, count, setCount }}>
            {children}
        </AppContext.Provider>
    );
}

פתרון: הפרדת Contexts

// ✅ הפרדה לפי תחום
const UserContext = createContext(null);
const CounterContext = createContext(null);

function Providers({ children }) {
    return (
        <UserProvider>
            <CounterProvider>
                {children}
            </CounterProvider>
        </UserProvider>
    );
}

פתרון: useMemo

function AppProvider({ children }) {
    const [user, setUser] = useState(null);

    // מניעת יצירת אובייקט חדש בכל רינדור
    const value = useMemo(() => ({ user, setUser }), [user]);

    return (
        <AppContext.Provider value={value}>
            {children}
        </AppContext.Provider>
    );
}

📊 Consumer (סגנון ישן)

לפני שהגיע ה-Hook בשם useContext, צרכו את ה-Context בעזרת רכיב Consumer ופונקציית render. התחביר עדיין נתמך, אך useContext קריא ונוח בהרבה ומומלץ לכל קוד חדש:

// סגנון ישן עם Consumer
<ThemeContext.Consumer>
    {theme => (
        <div className={theme}>תוכן</div>
    )}
</ThemeContext.Consumer>

// סגנון מודרני עם useContext
function Component() {
    const theme = useContext(ThemeContext);
    return <div className={theme}>תוכן</div>;
}

📋 סיכום

פעולה קוד
יצירה const Ctx = createContext(default)
הספקה <Ctx.Provider value={val}>
צריכה const val = useContext(Ctx)
Custom Hook useMyContext() עם בדיקת שגיאה

הקודם: ← Component Patterns
הבא: Custom Hooks →

🪝 Custom Hooks

מבוא

Custom Hook הוא פונקציה ששמה מתחיל ב-use ומשתמשת בתוכה ב-Hooks אחרים. תפקידו לחלץ לוגיקה החוזרת על עצמה - כמו שליפת נתונים מהשרת או האזנה לגודל החלון - למקום אחד, כדי שאפשר יהיה לעשות בה שימוש חוזר במספר קומפוננטות בלי לשכפל קוד. זהו אחד הכלים העוצמתיים ביותר לשמירה על קוד נקי ומאורגן ב-React.

// במקום לשכפל קוד...
function Component1() {
    const [loading, setLoading] = useState(true);
    const [data, setData] = useState(null);
    useEffect(() => { /* fetch logic */ }, []);
}

function Component2() {
    const [loading, setLoading] = useState(true);
    const [data, setData] = useState(null);
    useEffect(() => { /* same fetch logic */ }, []);
}

// ...יוצרים Custom Hook!
function useFetch(url) {
    const [loading, setLoading] = useState(true);
    const [data, setData] = useState(null);
    useEffect(() => { /* fetch logic */ }, [url]);
    return { loading, data };
}

function Component1() {
    const { loading, data } = useFetch('/api/data1');
}

📌 כללים ל-Custom Hooks

  1. שם מתחיל ב-use - חובה! (useMyHook, useFetch, useAuth)
  2. קורא ל-Hooks אחרים - useState, useEffect, וכו'
  3. מחזיר ערכים - מה שהקומפוננטה צריכה
  4. לוגיקה טהורה - ללא JSX (זה לא קומפוננטה!)

📝 דוגמאות Custom Hooks

1. useFetch - קריאות API

function useFetch(url) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        const controller = new AbortController();

        async function fetchData() {
            try {
                setLoading(true);
                setError(null);

                const response = await fetch(url, {
                    signal: controller.signal
                });

                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                const json = await response.json();
                setData(json);
            } catch (err) {
                if (err.name !== 'AbortError') {
                    setError(err.message);
                }
            } finally {
                setLoading(false);
            }
        }

        fetchData();

        return () => controller.abort();
    }, [url]);

    return { data, loading, error };
}

// שימוש
function UserList() {
    const { data: users, loading, error } = useFetch('/api/users');

    if (loading) return <p>טוען...</p>;
    if (error) return <p>שגיאה: {error}</p>;

    return (
        <ul>
            {users.map(user => <li key={user.id}>{user.name}</li>)}
        </ul>
    );
}

2. useLocalStorage - שמירה מקומית

function useLocalStorage(key, initialValue) {
    // קריאה מ-localStorage בטעינה
    const [value, setValue] = useState(() => {
        try {
            const item = localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch {
            return initialValue;
        }
    });

    // שמירה ל-localStorage בכל שינוי
    useEffect(() => {
        try {
            localStorage.setItem(key, JSON.stringify(value));
        } catch (error) {
            console.error('Error saving to localStorage:', error);
        }
    }, [key, value]);

    return [value, setValue];
}

// שימוש
function Settings() {
    const [theme, setTheme] = useLocalStorage('theme', 'light');
    const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);

    return (
        <div>
            <select value={theme} onChange={e => setTheme(e.target.value)}>
                <option value="light">בהיר</option>
                <option value="dark">כהה</option>
            </select>

            <input 
                type="range" 
                value={fontSize}
                onChange={e => setFontSize(Number(e.target.value))}
                min="12" max="24"
            />
        </div>
    );
}

3. useToggle - החלפת מצב

function useToggle(initialValue = false) {
    const [value, setValue] = useState(initialValue);

    const toggle = useCallback(() => {
        setValue(v => !v);
    }, []);

    const setTrue = useCallback(() => setValue(true), []);
    const setFalse = useCallback(() => setValue(false), []);

    return { value, toggle, setTrue, setFalse };
}

// שימוש
function Modal() {
    const { value: isOpen, toggle, setFalse: close } = useToggle();

    return (
        <>
            <button onClick={toggle}>פתח מודל</button>
            {isOpen && (
                <div className="modal">
                    <p>תוכן המודל</p>
                    <button onClick={close}>סגור</button>
                </div>
            )}
        </>
    );
}

4. useDebounce - השהייה

function useDebounce(value, delay = 300) {
    const [debouncedValue, setDebouncedValue] = useState(value);

    useEffect(() => {
        const timer = setTimeout(() => {
            setDebouncedValue(value);
        }, delay);

        return () => clearTimeout(timer);
    }, [value, delay]);

    return debouncedValue;
}

// שימוש - חיפוש עם השהייה
function SearchInput() {
    const [query, setQuery] = useState('');
    const debouncedQuery = useDebounce(query, 500);
    const { data: results } = useFetch(
        debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
    );

    return (
        <div>
            <input 
                value={query}
                onChange={e => setQuery(e.target.value)}
                placeholder="חפש..."
            />
            {results && <SearchResults results={results} />}
        </div>
    );
}

5. useWindowSize - גודל חלון

function useWindowSize() {
    const [size, setSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight
    });

    useEffect(() => {
        const handleResize = () => {
            setSize({
                width: window.innerWidth,
                height: window.innerHeight
            });
        };

        window.addEventListener('resize', handleResize);
        return () => window.removeEventListener('resize', handleResize);
    }, []);

    return size;
}

// שימוש
function ResponsiveComponent() {
    const { width } = useWindowSize();

    return (
        <div>
            {width < 768 ? <MobileLayout /> : <DesktopLayout />}
        </div>
    );
}

6. useClickOutside - לחיצה מחוץ לאלמנט

function useClickOutside(ref, callback) {
    useEffect(() => {
        const handleClick = (event) => {
            if (ref.current && !ref.current.contains(event.target)) {
                callback();
            }
        };

        document.addEventListener('mousedown', handleClick);
        return () => document.removeEventListener('mousedown', handleClick);
    }, [ref, callback]);
}

// שימוש
function Dropdown() {
    const [isOpen, setIsOpen] = useState(false);
    const dropdownRef = useRef(null);

    useClickOutside(dropdownRef, () => setIsOpen(false));

    return (
        <div ref={dropdownRef}>
            <button onClick={() => setIsOpen(!isOpen)}>תפריט</button>
            {isOpen && (
                <ul className="dropdown-menu">
                    <li>אפשרות 1</li>
                    <li>אפשרות 2</li>
                </ul>
            )}
        </div>
    );
}

7. usePrevious - ערך קודם

function usePrevious(value) {
    const ref = useRef();

    useEffect(() => {
        ref.current = value;
    }, [value]);

    return ref.current;
}

// שימוש
function Counter() {
    const [count, setCount] = useState(0);
    const prevCount = usePrevious(count);

    return (
        <div>
            <p>נוכחי: {count}</p>
            <p>קודם: {prevCount}</p>
            <button onClick={() => setCount(c => c + 1)}>+1</button>
        </div>
    );
}

8. useForm - ניהול טפסים

function useForm(initialValues, validate) {
    const [values, setValues] = useState(initialValues);
    const [errors, setErrors] = useState({});
    const [touched, setTouched] = useState({});

    const handleChange = (name) => (e) => {
        const value = e.target.type === 'checkbox' 
            ? e.target.checked 
            : e.target.value;

        setValues(prev => ({ ...prev, [name]: value }));
    };

    const handleBlur = (name) => () => {
        setTouched(prev => ({ ...prev, [name]: true }));

        if (validate) {
            const fieldError = validate({ [name]: values[name] });
            setErrors(prev => ({ ...prev, ...fieldError }));
        }
    };

    const handleSubmit = (onSubmit) => (e) => {
        e.preventDefault();

        if (validate) {
            const allErrors = validate(values);
            setErrors(allErrors);

            if (Object.keys(allErrors).length === 0) {
                onSubmit(values);
            }
        } else {
            onSubmit(values);
        }
    };

    const reset = () => {
        setValues(initialValues);
        setErrors({});
        setTouched({});
    };

    return {
        values,
        errors,
        touched,
        handleChange,
        handleBlur,
        handleSubmit,
        reset
    };
}

// שימוש
function LoginForm() {
    const validate = (values) => {
        const errors = {};
        if (!values.email?.includes('@')) errors.email = 'אימייל לא תקין';
        if (values.password?.length < 6) errors.password = 'לפחות 6 תווים';
        return errors;
    };

    const { values, errors, handleChange, handleBlur, handleSubmit } = useForm(
        { email: '', password: '' },
        validate
    );

    return (
        <form onSubmit={handleSubmit(data => console.log(data))}>
            <input 
                value={values.email}
                onChange={handleChange('email')}
                onBlur={handleBlur('email')}
            />
            {errors.email && <span>{errors.email}</span>}

            <input 
                type="password"
                value={values.password}
                onChange={handleChange('password')}
                onBlur={handleBlur('password')}
            />
            {errors.password && <span>{errors.password}</span>}

            <button type="submit">התחבר</button>
        </form>
    );
}

📋 סיכום

Hook שימוש
useFetch קריאות API
useLocalStorage שמירה מקומית
useToggle מצב בוליאני
useDebounce השהיית ערך
useWindowSize גודל חלון
useClickOutside לחיצה מחוץ
usePrevious ערך קודם
useForm ניהול טפסים

הקודם: ← Context API
הבא: React Patterns →

🎨 React Patterns (HOC, Render Props, Error Boundaries)

מבוא

דפוסי עיצוב (Design Patterns) הם פתרונות מוכחים לבעיות שחוזרות שוב ושוב בפיתוח. ב-React התגבשו לאורך השנים כמה דפוסים מרכזיים לשיתוף לוגיקה ולטיפול במצבים מיוחדים. בשיעור זה נכיר את HOC, Render Props, Error Boundaries ו-Fragments, ונבין מתי כדאי להשתמש בכל אחד מהם.


📌 HOC - Higher Order Component

HOC (רכיב מסדר גבוה) הוא פונקציה שמקבלת קומפוננטה ומחזירה קומפוננטה חדשה, "עטופה", עם יכולות נוספות. זו דרך להוסיף התנהגות משותפת (כמו בדיקת הרשאות או מצב טעינה) למספר קומפוננטות, בלי לשכתב כל אחת מהן. כיום Custom Hooks מחליפים את רוב השימושים ב-HOC, אך הדפוס עדיין נפוץ בספריות רבות:

התחביר

// HOC = פונקציה שמקבלת קומפוננטה
const withEnhancement = (WrappedComponent) => {
    // ומחזירה קומפוננטה חדשה
    return function EnhancedComponent(props) {
        // עם לוגיקה נוספת
        return <WrappedComponent {...props} extraProp="value" />;
    };
};

דוגמה: withLoading

// HOC שמוסיף מצב טעינה
function withLoading(WrappedComponent) {
    return function WithLoadingComponent({ isLoading, ...props }) {
        if (isLoading) {
            return (
                <div className="loading">
                    <span className="spinner">⏳</span>
                    <p>טוען...</p>
                </div>
            );
        }

        return <WrappedComponent {...props} />;
    };
}

// קומפוננטה רגילה
function UserList({ users }) {
    return (
        <ul>
            {users.map(user => <li key={user.id}>{user.name}</li>)}
        </ul>
    );
}

// קומפוננטה משופרת
const UserListWithLoading = withLoading(UserList);

// שימוש
function App() {
    const [loading, setLoading] = useState(true);
    const [users, setUsers] = useState([]);

    return <UserListWithLoading isLoading={loading} users={users} />;
}

דוגמה: withAuth

function withAuth(WrappedComponent) {
    return function AuthenticatedComponent(props) {
        const { isAuthenticated, loading } = useAuth();

        if (loading) {
            return <p>בודק הרשאות...</p>;
        }

        if (!isAuthenticated) {
            return <Navigate to="/login" />;
        }

        return <WrappedComponent {...props} />;
    };
}

// שימוש
const ProtectedDashboard = withAuth(Dashboard);
const ProtectedSettings = withAuth(Settings);

דוגמה: withLogger

function withLogger(WrappedComponent) {
    return function LoggedComponent(props) {
        useEffect(() => {
            console.log(`${WrappedComponent.name} mounted`);
            return () => console.log(`${WrappedComponent.name} unmounted`);
        }, []);

        useEffect(() => {
            console.log(`${WrappedComponent.name} props:`, props);
        });

        return <WrappedComponent {...props} />;
    };
}

📌 Render Props

Render Props הוא דפוס שבו קומפוננטה מקבלת פונקציה כ-prop (לרוב ה-children) וקוראת לה עם הנתונים הפנימיים שלה. כך הקומפוננטה מנהלת את הלוגיקה, ומי שמשתמש בה שולט בתצוגה - שיתוף לוגיקה תוך שמירה על גמישות מלאה בעיצוב:

התחביר

// קומפוננטה עם render prop
function DataProvider({ render }) {
    const [data, setData] = useState(null);

    // לוגיקה לקבלת data...

    return render(data);
}

// שימוש
<DataProvider 
    render={(data) => <DisplayComponent data={data} />}
/>

דוגמה: Mouse Position

function MouseTracker({ children }) {
    const [position, setPosition] = useState({ x: 0, y: 0 });

    useEffect(() => {
        const handleMouseMove = (e) => {
            setPosition({ x: e.clientX, y: e.clientY });
        };

        window.addEventListener('mousemove', handleMouseMove);
        return () => window.removeEventListener('mousemove', handleMouseMove);
    }, []);

    // קוראים ל-children כפונקציה עם הנתונים
    return children(position);
}

// שימוש
function App() {
    return (
        <MouseTracker>
            {({ x, y }) => (
                <div>
                    <p>מיקום: {x}, {y}</p>
                    <div 
                        style={{
                            position: 'absolute',
                            left: x - 10,
                            top: y - 10,
                            width: 20,
                            height: 20,
                            background: 'red',
                            borderRadius: '50%'
                        }}
                    />
                </div>
            )}
        </MouseTracker>
    );
}

דוגמה: Toggle

function Toggle({ children }) {
    const [isOn, setIsOn] = useState(false);

    const toggle = () => setIsOn(prev => !prev);

    return children({ isOn, toggle });
}

// שימוש
function App() {
    return (
        <Toggle>
            {({ isOn, toggle }) => (
                <div>
                    <button onClick={toggle}>
                        {isOn ? 'כבה' : 'הפעל'}
                    </button>
                    {isOn && <p>התוכן מוצג!</p>}
                </div>
            )}
        </Toggle>
    );
}

📌 Error Boundaries

Error Boundary הוא קומפוננטה שתופסת שגיאות JavaScript שמתרחשות בקומפוננטות שמתחתיה בעץ, ומציגה במקומן ממשק חלופי במקום שכל האפליקציה תקרוס. אפשר לחשוב עליו כמו try/catch ברמת ה-UI.

⚠️ Error Boundary הוא היוצא מן הכלל היחיד שבו עדיין נדרשת קומפוננטת מחלקה (Class Component), משום שהוא מסתמך על מתודות מחזור חיים שאין להן חלופה ב-Hooks.

התחביר

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false, error: null };
    }

    // נקרא כשיש שגיאה בילד
    static getDerivedStateFromError(error) {
        return { hasError: true, error };
    }

    // לוגים ודיווח
    componentDidCatch(error, errorInfo) {
        console.error('Error caught:', error);
        console.error('Error info:', errorInfo);
        // שליחה לשירות דיווח שגיאות
    }

    render() {
        if (this.state.hasError) {
            return this.props.fallback || (
                <div className="error-fallback">
                    <h2>😰 משהו השתבש</h2>
                    <p>אנא רענן את הדף</p>
                    <button onClick={() => window.location.reload()}>
                        רענן
                    </button>
                </div>
            );
        }

        return this.props.children;
    }
}

שימוש

function App() {
    return (
        <ErrorBoundary fallback={<p>שגיאה באפליקציה</p>}>
            <Header />
            <ErrorBoundary fallback={<p>שגיאה בתוכן</p>}>
                <MainContent />
            </ErrorBoundary>
            <Footer />
        </ErrorBoundary>
    );
}

// קומפוננטה שעלולה לזרוק שגיאה
function BuggyComponent() {
    const [shouldError, setShouldError] = useState(false);

    if (shouldError) {
        throw new Error('שגיאה מכוונת!');
    }

    return (
        <button onClick={() => setShouldError(true)}>
            גרום לשגיאה
        </button>
    );
}

Error Boundary עם Reset

class ErrorBoundaryWithReset extends React.Component {
    state = { hasError: false };

    static getDerivedStateFromError() {
        return { hasError: true };
    }

    handleReset = () => {
        this.setState({ hasError: false });
    }

    render() {
        if (this.state.hasError) {
            return (
                <div>
                    <h2>אופס! משהו השתבש</h2>
                    <button onClick={this.handleReset}>נסה שוב</button>
                </div>
            );
        }

        return this.props.children;
    }
}

📌 Fragments

כבר פגשנו את Fragment בשיעור על JSX. כתזכורת: הוא מאפשר לקבץ כמה אלמנטים תחת אלמנט שורש יחיד, בלי להוסיף תגית עוטפת מיותרת ל-DOM - מה שחשוב במיוחד בהקשרים כמו טבלאות ורשימות, שבהם div נוסף עלול לשבור את המבנה:

תחביר

// תחביר מלא
import { Fragment } from 'react';

function List() {
    return (
        <Fragment>
            <li>פריט 1</li>
            <li>פריט 2</li>
            <li>פריט 3</li>
        </Fragment>
    );
}

// תחביר מקוצר (נפוץ יותר)
function List() {
    return (
        <>
            <li>פריט 1</li>
            <li>פריט 2</li>
            <li>פריט 3</li>
        </>
    );
}

Fragment עם Key

// כשצריך key, חייבים תחביר מלא
function Glossary({ items }) {
    return (
        <dl>
            {items.map(item => (
                <Fragment key={item.id}>
                    <dt>{item.term}</dt>
                    <dd>{item.description}</dd>
                </Fragment>
            ))}
        </dl>
    );
}

למה להשתמש ב-Fragments?

// ❌ בעייתי - div מיותר שמשפיע על CSS
function Columns() {
    return (
        <div>  {/* div מיותר! */}
            <td>עמודה 1</td>
            <td>עמודה 2</td>
        </div>
    );
}

// ✅ נכון - Fragment לא יוצר אלמנט
function Columns() {
    return (
        <>
            <td>עמודה 1</td>
            <td>עמודה 2</td>
        </>
    );
}

📊 השוואה: HOC vs Render Props vs Custom Hooks

דפוס יתרונות חסרונות מתי להשתמש
HOC שימוש חוזר, הפרדה Wrapper hell, props conflict Cross-cutting concerns
Render Props גמישות, שקיפות Callback hell, פחות קריא UI דינמי מאוד
Custom Hooks פשוט, קריא, מודרני רק לוגיקה (לא UI) מועדף ברוב המקרים

המרה מ-HOC ל-Custom Hook

// HOC (ישן)
function withWindowSize(WrappedComponent) {
    return function(props) {
        const [size, setSize] = useState(getSize());
        useEffect(() => { /* listener */ }, []);
        return <WrappedComponent {...props} windowSize={size} />;
    };
}

// Custom Hook (מודרני ומועדף)
function useWindowSize() {
    const [size, setSize] = useState(getSize());
    useEffect(() => { /* listener */ }, []);
    return size;
}

📋 סיכום

Pattern תחביר
HOC withX(Component)
Render Props <Provider>{(data) => ...}</Provider>
Error Boundary Class with getDerivedStateFromError
Fragment <>...</> או <Fragment>

הקודם: ← Custom Hooks
הבא: React Router →

🧭 React Router

מבוא

React עצמה מטפלת ברכיב בודד, ולא בניווט בין דפים. כאן נכנסת React Router - ספריית הניתוב (Routing) הנפוצה ביותר לאפליקציות React. היא מאפשרת לבנות אפליקציית עמוד יחיד (SPA) שמתנהגת כאילו יש בה כמה דפים: הכתובת בשורת הדפדפן משתנה, אך במקום לטעון מחדש את כל העמוד, React Router פשוט מחליפה את הקומפוננטה המוצגת. התוצאה היא מעבר מהיר וחלק בין מסכים.

התקנה

npm install react-router-dom

📌 הגדרה בסיסית

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
    return (
        <BrowserRouter>
            {/* תפריט ניווט */}
            <nav>
                <Link to="/">בית</Link>
                <Link to="/about">אודות</Link>
                <Link to="/contact">צור קשר</Link>
            </nav>

            {/* הגדרת נתיבים */}
            <Routes>
                <Route path="/"         element={<Home />} />
                <Route path="/about"    element={<About />} />
                <Route path="/contact"  element={<Contact />} />
                <Route path="*"         element={<NotFound />} />
            </Routes>
        </BrowserRouter>
    );
}

function Home() {
    return <h1>דף הבית</h1>;
}

function About() {
    return <h1>אודות</h1>;
}

function Contact() {
    return <h1>צור קשר</h1>;
}

function NotFound() {
    return <h1>404 - הדף לא נמצא</h1>;
}

import { Link } from 'react-router-dom';

function Navigation() {
    return (
        <nav>
            <Link to="/">בית</Link>
            <Link to="/products">מוצרים</Link>
            <Link to="/about">אודות</Link>
        </nav>
    );
}
import { NavLink } from 'react-router-dom';

function Navigation() {
    return (
        <nav>
            <NavLink 
                to="/"
                className={({ isActive }) => isActive ? 'active' : ''}
            >
                בית
            </NavLink>

            <NavLink 
                to="/products"
                style={({ isActive }) => ({
                    fontWeight: isActive ? 'bold' : 'normal',
                    color: isActive ? 'blue' : 'black'
                })}
            >
                מוצרים
            </NavLink>
        </nav>
    );
}

🔀 Dynamic Routes (נתיבים דינמיים)

לעיתים קרובות נרצה נתיב שמשתנה לפי נתון - למשל עמוד פרופיל נפרד לכל משתמש. במקום להגדיר נתיב לכל אחד, מגדירים נתיב אחד עם פרמטר דינמי (המסומן בנקודתיים, למשל :userId), וקוראים את ערכו בעזרת ה-Hook בשם useParams:

הגדרת פרמטרים

function App() {
    return (
        <Routes>
            <Route path="/users"                        element={<UserList />} />
            <Route path="/users/:userId"                element={<UserProfile />} />
            <Route path="/products/:category/:productId"element={<Product />} />
        </Routes>
    );
}

קריאת פרמטרים עם useParams

import { useParams } from 'react-router-dom';

function UserProfile() {
    const { userId } = useParams();

    return <h1>פרופיל משתמש: {userId}</h1>;
}

function Product() {
    const { category, productId } = useParams();

    return (
        <div>
            <h1>קטגוריה: {category}</h1>
            <h2>מוצר: {productId}</h2>
        </div>
    );
}

דוגמה מלאה

function UserList() {
    const users = [
        { id: 1, name: 'דני' },
        { id: 2, name: 'רונית' },
        { id: 3, name: 'משה' }
    ];

    return (
        <div>
            <h1>רשימת משתמשים</h1>
            <ul>
                {users.map(user => (
                    <li key={user.id}>
                        <Link to={`/users/${user.id}`}>{user.name}</Link>
                    </li>
                ))}
            </ul>
        </div>
    );
}

function UserProfile() {
    const { userId } = useParams();
    const [user, setUser] = useState(null);

    useEffect(() => {
        fetch(`/api/users/${userId}`)
            .then(res => res.json())
            .then(setUser);
    }, [userId]);

    if (!user) return <p>טוען...</p>;

    return (
        <div>
            <h1>{user.name}</h1>
            <p>אימייל: {user.email}</p>
        </div>
    );
}

📁 Nested Routes (נתיבים מקוננים)

נתיבים מקוננים מאפשרים לשתף מבנה (Layout) בין כמה מסכים - למשל תפריט וכותרת קבועים, שבתוכם מתחלף רק התוכן. מגדירים נתיבי ילד בתוך נתיב הורה, וקומפוננטת ההורה מציינת בעזרת <Outlet /> היכן יוצג הנתיב הפעיל:

function App() {
    return (
        <Routes>
            <Route path="/"                 element={<Layout />}>
                <Route index                element={<Home />} />
                <Route path="about"         element={<About />} />
                <Route path="dashboard"     element={<Dashboard />}>
                    <Route index            element={<DashboardHome />} />
                    <Route path="profile"   element={<Profile />} />
                    <Route path="settings"  element={<Settings />} />
                </Route>
            </Route>
        </Routes>
    );
}

Outlet - הצגת נתיב ילד

import { Outlet, Link } from 'react-router-dom';

function Layout() {
    return (
        <div>
            <header>
                <nav>
                    <Link to="/">בית</Link>
                    <Link to="/dashboard">Dashboard</Link>
                </nav>
            </header>

            <main>
                <Outlet /> {/* כאן יוצג הנתיב הילד */}
            </main>

            <footer>פוטר</footer>
        </div>
    );
}

function Dashboard() {
    return (
        <div className="dashboard">
            <aside>
                <nav>
                    <Link to="/dashboard">סקירה</Link>
                    <Link to="/dashboard/profile">פרופיל</Link>
                    <Link to="/dashboard/settings">הגדרות</Link>
                </nav>
            </aside>

            <div className="content">
                <Outlet /> {/* תתי-נתיבים */}
            </div>
        </div>
    );
}

🔍 Query Parameters

פרמטרים של שאילתה (החלק שאחרי ה-? בכתובת, למשל ?q=react&page=2) מתאימים לנתונים אופציונליים כמו מונחי חיפוש, סינון ומספר עמוד. ה-Hook בשם useSearchParams מאפשר לקרוא ולעדכן אותם בדומה ל-useState:

import { useSearchParams } from 'react-router-dom';

function SearchResults() {
    const [searchParams, setSearchParams] = useSearchParams();

    // קריאה
    const query = searchParams.get('q');
    const page = searchParams.get('page') || '1';
    const sort = searchParams.get('sort') || 'date';

    // עדכון
    const updateSearch = (newQuery) => {
        setSearchParams({ q: newQuery, page: '1' });
    };

    const nextPage = () => {
        setSearchParams({
            q: query,
            page: String(Number(page) + 1),
            sort
        });
    };

    return (
        <div>
            <input 
                value={query || ''}
                onChange={(e) => updateSearch(e.target.value)}
                placeholder="חפש..."
            />
            <p>מציג תוצאות עבור: "{query}"</p>
            <p>עמוד: {page}</p>
            <button onClick={nextPage}>עמוד הבא</button>
        </div>
    );
}

🚀 Programmatic Navigation

לצד ניווט בלחיצה על קישור, לעיתים צריך לנווט מתוך הקוד עצמו - למשל להעביר את המשתמש ללוח הבקרה מיד לאחר התחברות מוצלחת. לשם כך משתמשים ב-Hook בשם useNavigate:

import { useNavigate } from 'react-router-dom';

function LoginForm() {
    const navigate = useNavigate();

    const handleSubmit = async (e) => {
        e.preventDefault();

        const success = await login(email, password);

        if (success) {
            // ניווט לאחר התחברות
            navigate('/dashboard');

            // או עם replace (לא נשמר בהיסטוריה)
            navigate('/dashboard', { replace: true });

            // או אחורה
            navigate(-1);
        }
    };

    return <form onSubmit={handleSubmit}>...</form>;
}

🔒 Protected Routes

נתיבים מוגנים חוסמים גישה למסכים מסוימים ממשתמשים שאינם מחוברים. מממשים זאת בעזרת קומפוננטת עטיפה שבודקת את מצב ההתחברות, ומפנה ל-login אם צריך - תוך שמירת היעד המקורי כדי לחזור אליו לאחר ההתחברות:

import { Navigate, useLocation } from 'react-router-dom';

function ProtectedRoute({ children }) {
    const { isAuthenticated, loading } = useAuth();
    const location = useLocation();

    if (loading) {
        return <p>טוען...</p>;
    }

    if (!isAuthenticated) {
        // שמירת המיקום המבוקש לאחר התחברות
        return <Navigate to="/login" state={{ from: location }} replace />;
    }

    return children;
}

// שימוש
function App() {
    return (
        <Routes>
            <Route path="/login" element={<Login />} />

            <Route 
                path="/dashboard" 
                element={
                    <ProtectedRoute>
                        <Dashboard />
                    </ProtectedRoute>
                }
            />
        </Routes>
    );
}

// ניווט חזרה אחרי התחברות
function Login() {
    const navigate = useNavigate();
    const location = useLocation();

    const from = location.state?.from?.pathname || '/';

    const handleLogin = async () => {
        await login();
        navigate(from, { replace: true });
    };
}

📍 useLocation Hook

import { useLocation } from 'react-router-dom';

function CurrentPath() {
    const location = useLocation();

    // location.pathname = '/users/123'
    // location.search = '?tab=profile'
    // location.hash = '#section1'
    // location.state = { from: ... }

    return (
        <div>
            <p>נתיב: {location.pathname}</p>
            <p>Query: {location.search}</p>
        </div>
    );
}

📋 סיכום

Component/Hook שימוש
BrowserRouter עוטף את האפליקציה
Routes מכיל את הנתיבים
Route הגדרת נתיב בודד
Link קישור לניווט
NavLink קישור עם active state
Outlet הצגת nested route
useParams קריאת פרמטרים דינמיים
useSearchParams query string
useNavigate ניווט בקוד
useLocation מידע על הנתיב הנוכחי

הקודם: ← React Patterns
הבא: Performance Optimization →

⚡ Performance Optimization

מבוא

React מהירה כברירת מחדל, אך באפליקציות גדולות רינדורים מיותרים עלולים להצטבר ולפגוע בחוויית המשתמש. בשיעור זה נכיר את שלושת הכלים המרכזיים לאופטימיזציה - React.memo, useMemo ו-useCallback. חשוב לזכור כלל אצבע: אין לבצע אופטימיזציה בטרם עת. משתמשים בכלים האלה כשמזהים בעיית ביצועים אמיתית, לא באופן גורף.


📌 React.memo

React.memo עוטף קומפוננטה ו"מזכיר" את התוצאה האחרונה שלה: אם ה-props לא השתנו מהרינדור הקודם, React מדלגת על הרינדור ומשתמשת בתוצאה השמורה. זה יעיל במיוחד לקומפוננטות "כבדות" שמקבלות את אותם props אך מתרנדרות שוב ושוב בגלל שינוי בהורה:

הבעיה

function Parent() {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('');

    return (
        <div>
            <button onClick={() => setCount(c => c + 1)}>
                {count}
            </button>

            {/* ExpensiveList מתרנדר בכל שינוי של count! */}
            <ExpensiveList items={items} />
        </div>
    );
}

function ExpensiveList({ items }) {
    console.log('ExpensiveList rendered!'); // נקרא בכל רינדור
    return <ul>{items.map(item => <li key={item}>{item}</li>)}</ul>;
}

הפתרון

// עטיפה ב-React.memo
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
    console.log('ExpensiveList rendered!'); // נקרא רק כש-items משתנה
    return <ul>{items.map(item => <li key={item}>{item}</li>)}</ul>;
});

Custom Comparison

const MemoizedComponent = React.memo(
    function MyComponent({ user, onClick }) {
        return <div onClick={onClick}>{user.name}</div>;
    },
    // Custom comparison function
    (prevProps, nextProps) => {
        // return true = לא לרנדר מחדש
        // return false = לרנדר מחדש
        return prevProps.user.id === nextProps.user.id;
    }
);

📌 useMemo

אם React.memo מזכיר קומפוננטה שלמה, useMemo מזכיר תוצאה של חישוב בודד בתוך קומפוננטה. הוא שומר את הערך המחושב ומריץ את החישוב מחדש רק כשאחת התלויות משתנה - שימושי לחישובים כבדים או לייצוב אובייקטים ומערכים שמועברים כ-props:

מתי להשתמש?

  • חישובים כבדים
  • יצירת אובייקטים/מערכים גדולים
  • מניעת רינדורים מיותרים ב-children

התחביר

const memoizedValue = useMemo(() => {
    return computeExpensiveValue(a, b);
}, [a, b]); // מחושב מחדש רק כש-a או b משתנים

דוגמאות

function ProductList({ products, filter, sort }) {
    // ❌ מחושב מחדש בכל רינדור
    const filteredProducts = products
        .filter(p => p.category === filter)
        .sort((a, b) => sort === 'price' ? a.price - b.price : a.name.localeCompare(b.name));

    // ✅ מחושב מחדש רק כשצריך
    const filteredProducts = useMemo(() => {
        console.log('Filtering and sorting...');
        return products
            .filter(p => p.category === filter)
            .sort((a, b) => sort === 'price' ? a.price - b.price : a.name.localeCompare(b.name));
    }, [products, filter, sort]);

    return (
        <ul>
            {filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
        </ul>
    );
}

מניעת רינדור ילדים

function Parent() {
    const [count, setCount] = useState(0);

    // ❌ אובייקט חדש בכל רינדור
    const style = { color: 'red', fontSize: 20 };

    // ✅ אותו reference
    const style = useMemo(() => ({ color: 'red', fontSize: 20 }), []);

    return (
        <div>
            <button onClick={() => setCount(c => c + 1)}>{count}</button>
            <MemoizedChild style={style} />
        </div>
    );
}

📌 useCallback

useCallback הוא המקבילה של useMemo עבור פונקציות: הוא שומר את אותה פונקציה בין רינדורים ויוצר חדשה רק כשהתלויות משתנות. זה קריטי כשמעבירים פונקציה לקומפוננטת ילד עטופה ב-React.memo, שכן פונקציה חדשה בכל רינדור הייתה מבטלת את היתרון שלה:

למה זה חשוב?

function Parent() {
    const [count, setCount] = useState(0);

    // ❌ פונקציה חדשה בכל רינדור
    const handleClick = () => {
        console.log('clicked');
    };

    // ✅ אותה פונקציה (אותו reference)
    const handleClick = useCallback(() => {
        console.log('clicked');
    }, []);

    return <MemoizedButton onClick={handleClick} />;
}

const MemoizedButton = React.memo(function Button({ onClick }) {
    console.log('Button rendered');
    return <button onClick={onClick}>לחץ</button>;
});

עם תלויות

function SearchComponent({ query }) {
    const [results, setResults] = useState([]);

    // פונקציה שתלויה ב-query
    const search = useCallback(async () => {
        const data = await fetchResults(query);
        setResults(data);
    }, [query]); // נוצרת מחדש רק כש-query משתנה

    useEffect(() => {
        search();
    }, [search]);
}

🔄 השוואה: useMemo vs useCallback

// useMemo - שומר ערך
const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);

// useCallback - שומר פונקציה
const memoizedFn = useCallback(() => doSomething(a, b), [a, b]);

// useCallback זה בעצם:
const memoizedFn = useMemo(() => () => doSomething(a, b), [a, b]);

📊 מתי להשתמש?

כלי מתי להשתמש
React.memo קומפוננטה שמתרנדרת הרבה עם אותם props
useMemo חישוב כבד / אובייקט שמועבר כ-prop
useCallback פונקציה שמועברת ל-memoized child

כלל אצבע

// ❌ לא צריך - פשוט מדי
const doubled = useMemo(() => count * 2, [count]);

// ✅ כן צריך - חישוב כבד
const filteredList = useMemo(() => 
    hugeList.filter(item => complexFilter(item, criteria)),
    [hugeList, criteria]
);

⚠️ טעויות נפוצות

1. Memoization מיותרת

// ❌ לא יעזור - הקומפוננטה ההורה לא memo
const MemoizedChild = React.memo(Child);

function Parent() {
    return <MemoizedChild data={data} />;
}

2. Dependencies חסרות

// ❌ שגוי - count לא ב-dependencies
const handleClick = useCallback(() => {
    console.log(count); // תמיד הערך הראשוני!
}, []);

// ✅ נכון
const handleClick = useCallback(() => {
    console.log(count);
}, [count]);

3. אובייקט inline

// ❌ אובייקט חדש בכל רינדור - memo לא עוזר
<MemoizedChild options={{ sort: true }} />

// ✅ useMemo
const options = useMemo(() => ({ sort: true }), []);
<MemoizedChild options={options} />

🛠️ כלי Debug

React DevTools Profiler

// הוספת displayName לזיהוי קל
const MemoizedComponent = React.memo(function MyComponent(props) {
    return <div>{props.value}</div>;
});
MemoizedComponent.displayName = 'MemoizedComponent';

Why Did You Render

npm install @welldone-software/why-did-you-render
// wdyr.js
import React from 'react';
if (process.env.NODE_ENV === 'development') {
    const whyDidYouRender = require('@welldone-software/why-did-you-render');
    whyDidYouRender(React, { trackAllPureComponents: true });
}

// Component.jsx
MyComponent.whyDidYouRender = true;

📋 סיכום

Hook/API תחביר שימוש
React.memo React.memo(Component) מניעת רינדור
useMemo useMemo(() => value, [deps]) שמירת ערך
useCallback useCallback(fn, [deps]) שמירת פונקציה

הקודם: ← React Router
הבא: Lazy Loading & Suspense →

📦 Lazy Loading, Suspense & Code Splitting

מבוא

כברירת מחדל, כל קוד האפליקציה נארז לקובץ אחד גדול שהמשתמש מוריד בטעינה הראשונה. ככל שהאפליקציה גדלה, הטעינה מואטת. שלוש הטכניקות בשיעור זה פותרות זאת יחד:

  • Code Splitting - חלוקת הקוד למספר חבילות קטנות (chunks) במקום קובץ ענק אחד
  • Lazy Loading - טעינת קומפוננטה רק ברגע שהיא באמת נדרשת
  • Suspense - הצגת ממשק טעינה זמני בזמן שהקומפוננטה העצלה נטענת

📌 React.lazy

React.lazy עוטף ייבוא דינמי (import()) והופך קומפוננטה לכזו שנטענת רק בעת הצורך. הקוד שלה יופרד אוטומטית לחבילה נפרדת. מכיוון שהטעינה לוקחת רגע, חובה לעטוף את הקומפוננטה ב-Suspense עם ממשק טעינה חלופי:

התחביר

import React, { lazy, Suspense } from 'react';

// במקום import רגיל
// import HeavyComponent from './HeavyComponent';

// טעינה עצלה
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
    return (
        <Suspense fallback={<div>טוען...</div>}>
            <HeavyComponent />
        </Suspense>
    );
}

📌 Suspense

Suspense מציג תוכן חלופי (fallback) בזמן טעינה.

import { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
    return (
        <div>
            <h1>האפליקציה שלי</h1>

            <Suspense fallback={<LoadingSpinner />}>
                <LazyComponent />
            </Suspense>
        </div>
    );
}

function LoadingSpinner() {
    return (
        <div className="spinner-container">
            <div className="spinner"></div>
            <p>טוען...</p>
        </div>
    );
}

Suspense מקונן

function App() {
    return (
        <Suspense fallback={<FullPageLoader />}>
            <Header />

            <Suspense fallback={<ContentLoader />}>
                <MainContent />
            </Suspense>

            <Suspense fallback={<SidebarLoader />}>
                <Sidebar />
            </Suspense>
        </Suspense>
    );
}

📌 Code Splitting עם Routes

הדרך הנפוצה ביותר - חלוקה לפי דפים:

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// טעינה עצלה של כל דף
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
    return (
        <BrowserRouter>
            <Suspense fallback={<PageLoader />}>
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/about" element={<About />} />
                    <Route path="/dashboard" element={<Dashboard />} />
                    <Route path="/settings" element={<Settings />} />
                </Routes>
            </Suspense>
        </BrowserRouter>
    );
}

function PageLoader() {
    return (
        <div className="page-loader">
            <div className="progress-bar"></div>
        </div>
    );
}

📌 Named Exports

React.lazy מצפה לייבוא ברירת מחדל (default export). כדי לטעון בעצלתיים רכיב שיוצא כ-named export, יש "לעטוף" אותו ולהחזיר אותו כ-default:

// ❌ לא עובד ישירות
const MyComponent = lazy(() => import('./Components').MyComponent);

// ✅ פתרון
const MyComponent = lazy(() => 
    import('./Components').then(module => ({ default: module.MyComponent }))
);

📌 Error Handling

שילוב Suspense עם Error Boundary:

import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
    return (
        <ErrorBoundary fallback={<ErrorMessage />}>
            <Suspense fallback={<Loading />}>
                <LazyComponent />
            </Suspense>
        </ErrorBoundary>
    );
}

class ErrorBoundary extends React.Component {
    state = { hasError: false };

    static getDerivedStateFromError() {
        return { hasError: true };
    }

    render() {
        if (this.state.hasError) {
            return this.props.fallback;
        }
        return this.props.children;
    }
}

📌 Preloading

אפשר לשפר עוד את חוויית המשתמש על ידי טעינה מוקדמת של קומפוננטות שסביר שיידרשו בקרוב - למשל להתחיל לטעון את עמוד לוח הבקרה כבר כשהעכבר מרחף מעל הקישור אליו, כך שכשהמשתמש ילחץ הוא כבר יהיה מוכן:

const LazyDashboard = lazy(() => import('./Dashboard'));

function Navigation() {
    // טעינה מוקדמת כשהעכבר מעל הלינק
    const handleMouseEnter = () => {
        import('./Dashboard'); // מתחיל לטעון ברקע
    };

    return (
        <nav>
            <Link 
                to="/dashboard" 
                onMouseEnter={handleMouseEnter}
            >
                Dashboard
            </Link>
        </nav>
    );
}

Prefetch עם Webpack

// prefetch - עדיפות נמוכה (idle time)
const LazyComponent = lazy(() => 
    import(/* webpackPrefetch: true */ './Component')
);

// preload - עדיפות גבוהה (מיד)
const LazyComponent = lazy(() => 
    import(/* webpackPreload: true */ './Component')
);

📌 Dynamic Import עם תנאי

function FeatureComponent({ type }) {
    const [Component, setComponent] = useState(null);

    useEffect(() => {
        async function loadComponent() {
            let module;

            if (type === 'chart') {
                module = await import('./ChartComponent');
            } else if (type === 'table') {
                module = await import('./TableComponent');
            } else {
                module = await import('./DefaultComponent');
            }

            setComponent(() => module.default);
        }

        loadComponent();
    }, [type]);

    if (!Component) return <Loading />;

    return <Component />;
}

📊 ניתוח Bundle

webpack-bundle-analyzer

npm install --save-dev webpack-bundle-analyzer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default {
    plugins: [
        visualizer({
            open: true,
            filename: 'bundle-stats.html'
        })
    ]
};

📋 Best Practices

1. Route-based Splitting

// מומלץ - כל דף נטען בנפרד
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

2. Component-based Splitting

// קומפוננטות כבדות
const HeavyEditor = lazy(() => import('./HeavyEditor'));
const ChartLibrary = lazy(() => import('./ChartLibrary'));

3. Feature-based Splitting

// פיצ'רים שלא כולם משתמשים
const AdminPanel = lazy(() => import('./features/AdminPanel'));
const Analytics = lazy(() => import('./features/Analytics'));

📋 סיכום

API שימוש
React.lazy() טעינה דינמית של קומפוננטה
Suspense הצגת fallback בזמן טעינה
import() Dynamic import של מודול
webpackPrefetch טעינה ברקע (idle)
webpackPreload טעינה מוקדמת (מיד)

הקודם: ← Performance
הבא: useReducer →

🔀 useReducer Hook

מבוא

useReducer הוא Hook לניהול State מורכב. הוא מבוסס על אותו רעיון כמו Redux: במקום לעדכן את ה-State ישירות, שולחים "פעולה" (action) לפונקציה אחת מרכזית - ה-reducer - שאחראית על כל לוגיקת העדכון. הוא חלופה ל-useState, ומתאים יותר כאשר:

  • ה-State מכיל כמה ערכים הקשורים זה לזה
  • לוגיקת העדכון מורכבת ומתפצלת למקרים רבים
  • העדכון הבא תלוי במצב הקודם

📌 התחביר הבסיסי

const [state, dispatch] = useReducer(reducer, initialState);

הרכיבים:

  1. state - המצב הנוכחי
  2. dispatch - פונקציה לשליחת פעולות (actions)
  3. reducer - פונקציה שמחשבת את ה-state החדש
  4. initialState - ערך התחלתי

📌 Reducer Function

ה-reducer הוא לב המנגנון: פונקציה טהורה שמקבלת את ה-State הנוכחי ואת ה-action שנשלח, ומחזירה את ה-State החדש. בדרך כלל בונים אותה כ-switch שמטפל בכל סוג פעולה בנפרד, ותמיד מחזירים אובייקט State חדש במקום לשנות את הקיים:

function reducer(state, action) {
    switch (action.type) {
        case 'INCREMENT':
            return { ...state, count: state.count + 1 };
        case 'DECREMENT':
            return { ...state, count: state.count - 1 };
        case 'RESET':
            return { ...state, count: 0 };
        default:
            return state;
    }
}

📝 דוגמה: Counter

import { useReducer } from 'react';

// הגדרת reducer
function counterReducer(state, action) {
    switch (action.type) {
        case 'INCREMENT':
            return { count: state.count + 1 };
        case 'DECREMENT':
            return { count: state.count - 1 };
        case 'INCREMENT_BY':
            return { count: state.count + action.payload };
        case 'RESET':
            return { count: 0 };
        default:
            throw new Error(`Unknown action: ${action.type}`);
    }
}

// הערך ההתחלתי
const initialState = { count: 0 };

function Counter() {
    const [state, dispatch] = useReducer(counterReducer, initialState);

    return (
        <div>
            <p>ספירה: {state.count}</p>

            <button onClick={() => dispatch({ type: 'INCREMENT' })}>
                +1
            </button>

            <button onClick={() => dispatch({ type: 'DECREMENT' })}>
                -1
            </button>

            <button onClick={() => dispatch({ type: 'INCREMENT_BY', payload: 5 })}>
                +5
            </button>

            <button onClick={() => dispatch({ type: 'RESET' })}>
                אפס
            </button>
        </div>
    );
}

📝 דוגמה: Todo List

// סוגי פעולות
const ACTIONS = {
    ADD:            'ADD_TODO',
    TOGGLE:         'TOGGLE_TODO',
    DELETE:         'DELETE_TODO',
    EDIT:           'EDIT_TODO',
    CLEAR_COMPLETED:'CLEAR_COMPLETED'
};

// Reducer
function todoReducer(state, action) {
    switch (action.type) {
        case ACTIONS.ADD:
            return [
                ...state,
                {
                    id: Date.now(),
                    text: action.payload,
                    completed: false
                }
            ];

        case ACTIONS.TOGGLE:
            return state.map(todo =>
                todo.id === action.payload ?
                    { ...todo, completed: !todo.completed }
                    : todo
            );

        case ACTIONS.DELETE:
            return state.filter(todo => todo.id !== action.payload);

        case ACTIONS.EDIT:
            return state.map(todo =>
                todo.id === action.payload.id ?
                    { ...todo, text: action.payload.text }
                    : todo
            );

        case ACTIONS.CLEAR_COMPLETED:
            return state.filter(todo => !todo.completed);

        default:
            return state;
    }
}

// קומפוננטה
function TodoApp() {
    const [todos, dispatch] = useReducer(todoReducer, []);
    const [text, setText] = useState('');

    const handleAdd = () => {
        if (text.trim()) {
            dispatch({ type: ACTIONS.ADD, payload: text });
            setText('');
        }
    };

    return (
        <div>
            <input 
                value={text}
                onChange={(e) => setText(e.target.value)}
                onKeyPress={(e) => e.key === 'Enter' && handleAdd()}
            />
            <button onClick={handleAdd}>הוסף</button>

            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        <input 
                            type="checkbox"
                            checked={todo.completed}
                            onChange={() => dispatch({ 
                                type: ACTIONS.TOGGLE, 
                                payload: todo.id 
                            })}
                        />
                        <span style={{ 
                            textDecoration: todo.completed ? 'line-through' : 'none'
                        }}>
                            {todo.text}
                        </span>
                        <button onClick={() => dispatch({ 
                            type: ACTIONS.DELETE, 
                            payload: todo.id 
                        })}>
                            ❌
                        </button>
                    </li>
                ))}
            </ul>

            <button onClick={() => dispatch({ type: ACTIONS.CLEAR_COMPLETED })}>
                נקה הושלמו
            </button>
        </div>
    );
}

📝 דוגמה: Form State

const initialFormState = {
    name: '',
    email: '',
    password: '',
    errors: {},
    isSubmitting: false,
    isSubmitted: false
};

function formReducer(state, action) {
    switch (action.type) {
        case 'SET_FIELD':
            return {
                ...state,
                [action.field]: action.value,
                errors: {
                    ...state.errors,
                    [action.field]: null // ניקוי שגיאה בעת הקלדה
                }
            };

        case 'SET_ERROR':
            return {
                ...state,
                errors: {
                    ...state.errors,
                    [action.field]: action.error
                }
            };

        case 'SET_ERRORS':
            return {
                ...state,
                errors: action.errors
            };

        case 'SUBMIT_START':
            return {
                ...state,
                isSubmitting: true,
                errors: {}
            };

        case 'SUBMIT_SUCCESS':
            return {
                ...state,
                isSubmitting: false,
                isSubmitted: true
            };

        case 'SUBMIT_FAILURE':
            return {
                ...state,
                isSubmitting: false,
                errors: action.errors
            };

        case 'RESET':
            return initialFormState;

        default:
            return state;
    }
}

function RegistrationForm() {
    const [state, dispatch] = useReducer(formReducer, initialFormState);

    const handleChange = (field) => (e) => {
        dispatch({ type: 'SET_FIELD', field, value: e.target.value });
    };

    const validate = () => {
        const errors = {};
        if (!state.name) errors.name = 'שם נדרש';
        if (!state.email.includes('@')) errors.email = 'אימייל לא תקין';
        if (state.password.length < 6) errors.password = 'לפחות 6 תווים';
        return errors;
    };

    const handleSubmit = async (e) => {
        e.preventDefault();

        const errors = validate();
        if (Object.keys(errors).length > 0) {
            dispatch({ type: 'SET_ERRORS', errors });
            return;
        }

        dispatch({ type: 'SUBMIT_START' });

        try {
            await submitForm(state);
            dispatch({ type: 'SUBMIT_SUCCESS' });
        } catch (error) {
            dispatch({ 
                type: 'SUBMIT_FAILURE', 
                errors: { form: error.message }
            });
        }
    };

    if (state.isSubmitted) {
        return <p>✅ ההרשמה הצליחה!</p>;
    }

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <input 
                    value={state.name}
                    onChange={handleChange('name')}
                    placeholder="שם"
                />
                {state.errors.name && <span className="error">{state.errors.name}</span>}
            </div>

            <div>
                <input 
                    type="email"
                    value={state.email}
                    onChange={handleChange('email')}
                    placeholder="אימייל"
                />
                {state.errors.email && <span className="error">{state.errors.email}</span>}
            </div>

            <div>
                <input 
                    type="password"
                    value={state.password}
                    onChange={handleChange('password')}
                    placeholder="סיסמה"
                />
                {state.errors.password && <span className="error">{state.errors.password}</span>}
            </div>

            <button type="submit" disabled={state.isSubmitting}>
                {state.isSubmitting ? 'שולח...' : 'הרשמה'}
            </button>
        </form>
    );
}

🔄 useState vs useReducer

תכונה useState useReducer
מורכבות פשוט מורכב
מספר ערכים אחד או מעט הרבה קשורים
לוגיקת עדכון פשוטה מורכבת
תלות בערך קודם לא מומלץ מתאים
Testing קשה יותר קל יותר

מתי להשתמש ב-useReducer?

// ✅ useState - פשוט
const [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);

// ✅ useReducer - מורכב
const [formState, dispatch] = useReducer(formReducer, initialFormState);
// formState = { name, email, password, errors, isSubmitting, ... }

// ✅ useReducer - עדכונים תלויים
const [history, dispatch] = useReducer(historyReducer, { past: [], present: null, future: [] });

📌 useReducer עם Context

שילוב useReducer עם Context הוא דפוס עוצמתי ונפוץ: ה-reducer מרכז את כל לוגיקת העדכון, וה-Context מפיץ את ה-State ואת פונקציית ה-dispatch לכל האפליקציה. התוצאה היא ניהול State גלובלי מסודר, ללא ספרייה חיצונית - מעין "Redux מוקטן" מובנה:

const TodoContext = createContext();

function TodoProvider({ children }) {
    const [todos, dispatch] = useReducer(todoReducer, []);

    return (
        <TodoContext.Provider value={{ todos, dispatch }}>
            {children}
        </TodoContext.Provider>
    );
}

function useTodos() {
    return useContext(TodoContext);
}

// שימוש
function TodoList() {
    const { todos, dispatch } = useTodos();
    // ...
}

📋 סיכום

מושג הסבר
useReducer Hook לניהול state מורכב
reducer פונקציה (state, action) => newState
dispatch פונקציה לשליחת actions
action אובייקט עם type ו-payload
// תבנית בסיסית
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'ACTION_TYPE', payload: data });

הקודם: ← Lazy Loading
הבא: Redux Toolkit →

🏪 Redux Toolkit - יסודות

מבוא

Redux היא ספריית ניהול State גלובלי הנפוצה ביותר באקוסיסטם של React. היא מרכזת את כל מצב האפליקציה ב"מחסן" (store) יחיד, ומגדירה זרימת נתונים ברורה וצפויה לעדכונו. בעבר Redux נחשבה למרובת קוד ומסורבלת, ולכן צוות Redux יצר את Redux Toolkit (RTK) - הדרך הרשמית והמומלצת כיום, שמפחיתה דרמטית את כמות הקוד ומגיעה עם הגדרות מיטביות מובנות.

התקנה

npm install @reduxjs/toolkit react-redux

📌 מושגי יסוד

מושג הסבר
Store מאגר ה-state המרכזי
Action אובייקט שמתאר שינוי
Reducer פונקציה שמחשבת state חדש
Dispatch שליחת action ל-store
Selector פונקציה לקריאת state
Slice "פרוסה" של ה-store לתחום מסוים

📌 יצירת Store

// store/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
import todosReducer from './todosSlice';

// יצירת ה-store
const store = configureStore({
    reducer: {
        counter: counterReducer,
        todos: todosReducer
    }
});

export default store;

📌 יצירת Slice

Slice ("פרוסה") הוא לב העבודה עם RTK: הוא מרכז במקום אחד את שם התחום, ה-State ההתחלתי וה-reducers שלו. מתוך ה-reducers ש-createSlice מקבל, הוא מייצר אוטומטית את ה-actions המתאימים. בזכות ספריית Immer המובנית, אפשר לכתוב בתוך ה-reducers קוד שנראה כמו שינוי ישיר של ה-State - ומאחורי הקלעים נוצר State חדש ותקין:

// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
    name: 'counter',

    initialState: {
        value: 0
    },

    reducers: {
        // כל פונקציה היא reducer וגם יוצרת action
        increment: (state) => {
            // Immer מאפשר "מוטציה" כאילו!
            state.value += 1;
        },

        decrement: (state) => {
            state.value -= 1;
        },

        incrementByAmount: (state, action) => {
            state.value += action.payload;
        },

        reset: (state) => {
            state.value = 0;
        }
    }
});

// ייצוא ה-actions
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;

// ייצוא ה-reducer
export default counterSlice.reducer;

// Selector
export const selectCount = (state) => state.counter.value;

📌 Provider

כדי שכל הקומפוננטות יוכלו לגשת ל-store, עוטפים את שורש האפליקציה ברכיב Provider של react-redux ומעבירים לו את ה-store. זהו שלב חד-פעמי שמבוצע בנקודת הכניסה של האפליקציה:

// main.jsx / index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store/store';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <Provider store={store}>
            <App />
        </Provider>
    </React.StrictMode>
);

📌 שימוש בקומפוננטות

import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount, reset, selectCount } from './store/counterSlice';

function Counter() {
    // קריאה מה-store
    const count = useSelector(selectCount);

    // קבלת dispatch
    const dispatch = useDispatch();

    return (
        <div>
            <h2>ספירה: {count}</h2>

            <button onClick={() => dispatch(increment())}>
                +1
            </button>

            <button onClick={() => dispatch(decrement())}>
                -1
            </button>

            <button onClick={() => dispatch(incrementByAmount(10))}>
                +10
            </button>

            <button onClick={() => dispatch(reset())}>
                אפס
            </button>
        </div>
    );
}

📝 דוגמה מלאה: Todo Slice

// store/todosSlice.js
import { createSlice, nanoid } from '@reduxjs/toolkit';

const todosSlice = createSlice({
    name: 'todos',

    initialState: {
        items: [],
        filter: 'all', // 'all' | 'active' | 'completed'
        isLoading: false
    },

    reducers: {
        addTodo: {
            reducer: (state, action) => {
                state.items.push(action.payload);
            },
            // prepare מאפשר לעבד את ה-payload לפני הגעה ל-reducer
            prepare: (text) => ({
                payload: {
                    id: nanoid(),
                    text,
                    completed: false,
                    createdAt: new Date().toISOString()
                }
            })
        },

        toggleTodo: (state, action) => {
            const todo = state.items.find(t => t.id === action.payload);
            if (todo) {
                todo.completed = !todo.completed;
            }
        },

        editTodo: (state, action) => {
            const { id, text } = action.payload;
            const todo = state.items.find(t => t.id === id);
            if (todo) {
                todo.text = text;
            }
        },

        deleteTodo: (state, action) => {
            state.items = state.items.filter(t => t.id !== action.payload);
        },

        clearCompleted: (state) => {
            state.items = state.items.filter(t => !t.completed);
        },

        setFilter: (state, action) => {
            state.filter = action.payload;
        }
    }
});

export const { 
    addTodo, 
    toggleTodo, 
    editTodo, 
    deleteTodo, 
    clearCompleted, 
    setFilter 
} = todosSlice.actions;

export default todosSlice.reducer;

// Selectors
export const selectAllTodos = (state) => state.todos.items;
export const selectFilter = (state) => state.todos.filter;

export const selectFilteredTodos = (state) => {
    const { items, filter } = state.todos;

    switch (filter) {
        case 'active':
            return items.filter(t => !t.completed);
        case 'completed':
            return items.filter(t => t.completed);
        default:
            return items;
    }
};

export const selectTodosStats = (state) => {
    const items = state.todos.items;
    return {
        total: items.length,
        active: items.filter(t => !t.completed).length,
        completed: items.filter(t => t.completed).length
    };
};

שימוש

function TodoApp() {
    const todos = useSelector(selectFilteredTodos);
    const stats = useSelector(selectTodosStats);
    const filter = useSelector(selectFilter);
    const dispatch = useDispatch();
    const [text, setText] = useState('');

    const handleAdd = () => {
        if (text.trim()) {
            dispatch(addTodo(text));
            setText('');
        }
    };

    return (
        <div>
            <input 
                value={text}
                onChange={(e) => setText(e.target.value)}
                onKeyPress={(e) => e.key === 'Enter' && handleAdd()}
            />
            <button onClick={handleAdd}>הוסף</button>

            <div>
                <button 
                    className={filter === 'all' ? 'active' : ''}
                    onClick={() => dispatch(setFilter('all'))}
                >
                    הכל ({stats.total})
                </button>
                <button 
                    className={filter === 'active' ? 'active' : ''}
                    onClick={() => dispatch(setFilter('active'))}
                >
                    פעילות ({stats.active})
                </button>
                <button 
                    className={filter === 'completed' ? 'active' : ''}
                    onClick={() => dispatch(setFilter('completed'))}
                >
                    הושלמו ({stats.completed})
                </button>
            </div>

            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        <input 
                            type="checkbox"
                            checked={todo.completed}
                            onChange={() => dispatch(toggleTodo(todo.id))}
                        />
                        <span style={{ 
                            textDecoration: todo.completed ? 'line-through' : 'none' 
                        }}>
                            {todo.text}
                        </span>
                        <button onClick={() => dispatch(deleteTodo(todo.id))}>❌</button>
                    </li>
                ))}
            </ul>

            <button onClick={() => dispatch(clearCompleted())}>
                נקה הושלמו
            </button>
        </div>
    );
}

📋 סיכום

API שימוש
configureStore יצירת store
createSlice יצירת slice (reducer + actions)
Provider עטיפת האפליקציה
useSelector קריאה מה-store
useDispatch קבלת dispatch

הקודם: ← useReducer
הבא: Redux Hooks & Async →

🔌 Redux Hooks & Async Actions

מבוא

לאחר שהקמנו את ה-store וה-slices, הגיע הזמן להעמיק בעבודה השוטפת מול Redux מתוך הקומפוננטות. שיעור זה מכסה:

  • שימוש מעמיק ב-useSelector וב-useDispatch
  • פעולות אסינכרוניות (כמו קריאות שרת) בעזרת createAsyncThunk
  • Middleware - שכבת ביניים בזרימת ה-actions
  • דפוסי עבודה מתקדמים ומומלצים

📌 useSelector - קריאה מה-Store

ה-Hook בשם useSelector מאפשר לקומפוננטה לקרוא נתון מתוך ה-store. הוא מקבל פונקציית "בורר" (selector) שבוחרת את החלק הרלוונטי מה-State, והקומפוננטה תתרנדר מחדש רק כשאותו חלק ישתנה. כדי לא לגרום לרינדורים מיותרים, עדיף לבחור ערכים ספציפיים ולא להחזיר אובייקט חדש בכל קריאה:

import { useSelector } from 'react-redux';

function MyComponent() {
    // קריאה ישירה
    const count = useSelector(state => state.counter.value);

    // שימוש ב-selector מוגדר מראש
    const todos = useSelector(selectAllTodos);

    // Selector עם חישוב
    const total = useSelector(state => 
        state.cart.items.reduce((sum, item) => sum + item.price, 0)
    );

    // מספר ערכים (לא מומלץ - גורם לרינדורים מיותרים)
    const { user, isLoading } = useSelector(state => ({
        user: state.auth.user,
        isLoading: state.auth.isLoading
    }));
}

Selectors עם Reselect

import { createSelector } from '@reduxjs/toolkit';

// Selector בסיסי
const selectItems = state => state.cart.items;
const selectTaxRate = state => state.settings.taxRate;

// Selector מורכב עם memoization
const selectTotalWithTax = createSelector(
    [selectItems, selectTaxRate],
    (items, taxRate) => {
        const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
        return subtotal * (1 + taxRate);
    }
);

// שימוש
function CartTotal() {
    const total = useSelector(selectTotalWithTax);
    return <p>סה"כ: ₪{total.toFixed(2)}</p>;
}

📌 useDispatch - שליחת Actions

אם useSelector משמש לקריאה מה-store, useDispatch משמש לכתיבה אליו. הוא מחזיר את פונקציית ה-dispatch, שבאמצעותה שולחים actions כדי לעדכן את ה-State:

import { useDispatch } from 'react-redux';
import { increment, addTodo } from './store';

function MyComponent() {
    const dispatch = useDispatch();

    // שליחה פשוטה
    const handleClick = () => {
        dispatch(increment());
    };

    // שליחה עם payload
    const handleAdd = (text) => {
        dispatch(addTodo(text));
    };

    // שליחה מרובה
    const handleReset = () => {
        dispatch(resetCart());
        dispatch(clearUser());
        dispatch(showNotification('הנתונים אופסו'));
    };
}

📌 createAsyncThunk - פעולות אסינכרוניות

reducers רגילים חייבים להיות סינכרוניים, אך פעולות רבות (כמו שליפת נתונים מהשרת) לוקחות זמן. createAsyncThunk נועד בדיוק לכך: הוא עוטף פעולה אסינכרונית ומייצר עבורה שלושה actions אוטומטיים - pending (התחלה), fulfilled (הצלחה) ו-rejected (כישלון). את שלושתם מטפלים ב-slice דרך extraReducers, וכך מנהלים בקלות מצבי טעינה ושגיאה:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// יצירת async thunk
export const fetchUsers = createAsyncThunk(
    'users/fetchUsers', // שם ה-action
    async (_, { rejectWithValue }) => {
        try {
            const response = await fetch('/api/users');
            if (!response.ok) {
                throw new Error('שגיאה בטעינה');
            }
            return await response.json();
        } catch (error) {
            return rejectWithValue(error.message);
        }
    }
);

// עם פרמטרים
export const fetchUserById = createAsyncThunk(
    'users/fetchById',
    async (userId, { rejectWithValue }) => {
        try {
            const response = await fetch(`/api/users/${userId}`);
            return await response.json();
        } catch (error) {
            return rejectWithValue(error.message);
        }
    }
);

// עם גישה ל-state
export const addToCart = createAsyncThunk(
    'cart/addItem',
    async (product, { getState, rejectWithValue }) => {
        const { auth } = getState();

        if (!auth.user) {
            return rejectWithValue('יש להתחבר תחילה');
        }

        const response = await fetch('/api/cart', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${auth.token}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(product)
        });

        return await response.json();
    }
);

טיפול ב-Slice

const usersSlice = createSlice({
    name: 'users',

    initialState: {
        items: [],
        selectedUser: null,
        status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
        error: null
    },

    reducers: {
        clearError: (state) => {
            state.error = null;
        }
    },

    // טיפול ב-async actions
    extraReducers: (builder) => {
        builder
            // fetchUsers
            .addCase(fetchUsers.pending, (state) => {
                state.status = 'loading';
                state.error = null;
            })
            .addCase(fetchUsers.fulfilled, (state, action) => {
                state.status = 'succeeded';
                state.items = action.payload;
            })
            .addCase(fetchUsers.rejected, (state, action) => {
                state.status = 'failed';
                state.error = action.payload || action.error.message;
            })

            // fetchUserById
            .addCase(fetchUserById.pending, (state) => {
                state.status = 'loading';
            })
            .addCase(fetchUserById.fulfilled, (state, action) => {
                state.status = 'succeeded';
                state.selectedUser = action.payload;
            })
            .addCase(fetchUserById.rejected, (state, action) => {
                state.status = 'failed';
                state.error = action.payload;
            });
    }
});

שימוש בקומפוננטה

function UserList() {
    const dispatch = useDispatch();
    const { items: users, status, error } = useSelector(state => state.users);

    useEffect(() => {
        if (status === 'idle') {
            dispatch(fetchUsers());
        }
    }, [status, dispatch]);

    if (status === 'loading') {
        return <p>טוען משתמשים...</p>;
    }

    if (status === 'failed') {
        return <p>שגיאה: {error}</p>;
    }

    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
}

📌 Middleware

Middleware הוא שכבת ביניים שכל action עובר דרכה בדרך אל ה-reducer. זו נקודת הידוק אידיאלית לפעולות רוחביות כמו רישום ללוג (logging), דיווח שגיאות או ניתוב פעולות אסינכרוניות:

// store/middleware/logger.js
const loggerMiddleware = (store) => (next) => (action) => {
    console.group(action.type);
    console.log('dispatching:', action);
    console.log('prev state:', store.getState());

    const result = next(action);

    console.log('next state:', store.getState());
    console.groupEnd();

    return result;
};

// store/store.js
import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
    reducer: { /* ... */ },
    middleware: (getDefaultMiddleware) => 
        getDefaultMiddleware().concat(loggerMiddleware)
});

Middleware לדיווח שגיאות

const errorReportingMiddleware = (store) => (next) => (action) => {
    try {
        return next(action);
    } catch (error) {
        console.error('Error in reducer:', error);
        // שליחה לשירות דיווח שגיאות
        reportError(error, action);
        throw error;
    }
};

📌 Typed Hooks (TypeScript)

// store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Typed hooks
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// שימוש
function MyComponent() {
    const dispatch = useAppDispatch();
    const user = useAppSelector(state => state.auth.user); // עם autocomplete!
}

📌 דפוסי שימוש מתקדמים

Action Creator עם לוגיקה

// Thunk ידני (לא createAsyncThunk)
export const conditionalIncrement = () => (dispatch, getState) => {
    const { value } = getState().counter;

    if (value < 10) {
        dispatch(increment());
    }
};

export const incrementIfOdd = (amount) => (dispatch, getState) => {
    const { value } = getState().counter;

    if (value % 2 === 1) {
        dispatch(incrementByAmount(amount));
    }
};

Batch Updates

import { batch } from 'react-redux';

function handleSubmit() {
    // כל ה-dispatches יגרמו לרינדור אחד בלבד
    batch(() => {
        dispatch(updateUser(userData));
        dispatch(updateSettings(settings));
        dispatch(showNotification('נשמר בהצלחה'));
    });
}

📋 סיכום

API שימוש
useSelector(selector) קריאה מה-store
useDispatch() קבלת dispatch
createAsyncThunk async actions
extraReducers טיפול ב-thunk
createSelector memoized selectors
Middleware לוגיקה בין dispatch ל-reducer

הקודם: ← Redux Basics
הבא: RTK Query →

🔄 RTK Query

מבוא

RTK Query הוא כלי ייעודי לשליפת נתונים (data fetching) ולניהול מטמון (caching), המגיע כחלק מ-Redux Toolkit. הוא הופך את הדפוס המוכר של useState + useEffect + createAsyncThunk למיותר: מגדירים את נקודות הקצה (endpoints) פעם אחת, ומקבלים Hooks מוכנים שמטפלים עבורכם באופן אוטומטי ב:

  • מצבי טעינה ושגיאה (Loading / Error)
  • שמירת תוצאות במטמון ושיתופן בין קומפוננטות (Caching)
  • רענון נתונים אוטומטי בעת הצורך (Re-fetching)
  • עדכונים אופטימיים (Optimistic updates)

📌 הגדרת API

נקודת המוצא היא הגדרת "פרוסת API" בעזרת createApi. בתוכה מגדירים את כתובת הבסיס של השרת ואת נקודות הקצה, המחולקות לשני סוגים: query לקריאת נתונים (GET) ו-mutation לשינוי נתונים (POST/PUT/DELETE). מנגנון ה-tags מאפשר ל-RTK Query לדעת אילו נתונים במטמון להתייתר ולרענן לאחר שינוי. מכל אלה נוצרים אוטומטית ה-Hooks שבהם נשתמש בקומפוננטות:

// store/api/usersApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const usersApi = createApi({
    reducerPath: 'usersApi',

    baseQuery: fetchBaseQuery({ 
        baseUrl: '/api',
        prepareHeaders: (headers, { getState }) => {
            const token = getState().auth.token;
            if (token) {
                headers.set('Authorization', `Bearer ${token}`);
            }
            return headers;
        }
    }),

    tagTypes: ['User', 'Users'],

    endpoints: (builder) => ({
        // GET all users
        getUsers: builder.query({
            query: () => '/users',
            providesTags: ['Users']
        }),

        // GET single user
        getUserById: builder.query({
            query: (id) => `/users/${id}`,
            providesTags: (result, error, id) => [{ type: 'User', id }]
        }),

        // POST new user
        addUser: builder.mutation({
            query: (newUser) => ({
                url: '/users',
                method: 'POST',
                body: newUser
            }),
            invalidatesTags: ['Users']
        }),

        // PUT update user
        updateUser: builder.mutation({
            query: ({ id, ...patch }) => ({
                url: `/users/${id}`,
                method: 'PUT',
                body: patch
            }),
            invalidatesTags: (result, error, { id }) => [
                { type: 'User', id },
                'Users'
            ]
        }),

        // DELETE user
        deleteUser: builder.mutation({
            query: (id) => ({
                url: `/users/${id}`,
                method: 'DELETE'
            }),
            invalidatesTags: ['Users']
        })
    })
});

// ייצוא Hooks אוטומטיים
export const {
    useGetUsersQuery,
    useGetUserByIdQuery,
    useAddUserMutation,
    useUpdateUserMutation,
    useDeleteUserMutation
} = usersApi;

📌 הגדרת Store

// store/store.js
import { configureStore } from '@reduxjs/toolkit';
import { usersApi } from './api/usersApi';

const store = configureStore({
    reducer: {
        [usersApi.reducerPath]: usersApi.reducer,
        // reducers נוספים...
    },
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware().concat(usersApi.middleware)
});

export default store;

📌 שימוש - Query (GET)

ה-Hooks מסוג query משמשים לקריאת נתונים. הם מחזירים אובייקט עשיר הכולל את הנתונים עצמם (data), דגלי מצב (isLoading, isError) ופונקציית refetch לרענון ידני - כך שאין צורך לנהל את כל אלה בעצמנו:

import { useGetUsersQuery, useGetUserByIdQuery } from './store/api/usersApi';

function UserList() {
    const { 
        data: users, 
        isLoading, 
        isError, 
        error,
        isFetching,
        refetch 
    } = useGetUsersQuery();

    if (isLoading) return <p>טוען...</p>;
    if (isError) return <p>שגיאה: {error.message}</p>;

    return (
        <div>
            <button onClick={refetch} disabled={isFetching}>
                {isFetching ? 'מרענן...' : 'רענן'}
            </button>
            <ul>
                {users.map(user => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        </div>
    );
}

function UserProfile({ userId }) {
    const { data: user, isLoading } = useGetUserByIdQuery(userId, {
        // אפשרויות
        skip: !userId, // דלג אם אין userId
        pollingInterval: 30000, // רענון כל 30 שניות
        refetchOnMountOrArgChange: true
    });

    if (isLoading) return <p>טוען פרופיל...</p>;
    if (!user) return <p>משתמש לא נמצא</p>;

    return (
        <div>
            <h1>{user.name}</h1>
            <p>{user.email}</p>
        </div>
    );
}

📌 שימוש - Mutation (POST/PUT/DELETE)

ה-Hooks מסוג mutation משמשים לשינוי נתונים בשרת. בשונה מ-query, הם אינם רצים אוטומטית אלא מחזירים פונקציית הפעלה שקוראים לה בעת הצורך. שרשור .unwrap() מאפשר לתפוס הצלחה או שגיאה בעזרת try/catch רגיל:

import {
    useAddUserMutation, 
    useUpdateUserMutation, 
    useDeleteUserMutation 
} from './store/api/usersApi';

function AddUserForm() {
    const [addUser, { isLoading, isSuccess, isError, error }] = useAddUserMutation();
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');

    const handleSubmit = async (e) => {
        e.preventDefault();

        try {
            await addUser({ name, email }).unwrap();
            setName('');
            setEmail('');
            alert('המשתמש נוסף בהצלחה!');
        } catch (err) {
            alert('שגיאה: ' + err.message);
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <input 
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="שם"
            />
            <input 
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                placeholder="אימייל"
            />
            <button type="submit" disabled={isLoading}>
                {isLoading ? 'שומר...' : 'הוסף משתמש'}
            </button>
        </form>
    );
}

function UserItem({ user }) {
    const [updateUser] = useUpdateUserMutation();
    const [deleteUser, { isLoading: isDeleting }] = useDeleteUserMutation();

    const handleUpdate = async () => {
        const newName = prompt('שם חדש:', user.name);
        if (newName) {
            await updateUser({ id: user.id, name: newName });
        }
    };

    const handleDelete = async () => {
        if (confirm('למחוק את המשתמש?')) {
            await deleteUser(user.id);
        }
    };

    return (
        <li>
            {user.name}
            <button onClick={handleUpdate}>✏️ ערוך</button>
            <button onClick={handleDelete} disabled={isDeleting}>
                {isDeleting ? '...' : '❌ מחק'}
            </button>
        </li>
    );
}

📌 Tags ו-Cache Invalidation

מנגנון ה-Tags הוא שמחבר בין קריאות לשינויים. כל query "מספקת" תגית (providesTags), וכל mutation יכולה "לבטל" תגית (invalidatesTags). כשמבטלים תגית, RTK Query מרענן אוטומטית כל query שסיפקה אותה - כך התצוגה נשארת מסונכרנת עם השרת ללא רענון ידני:

// Tags מאפשרים לקשר בין queries ל-mutations
endpoints: (builder) => ({
    getPosts: builder.query({
        query: () => '/posts',
        providesTags: ['Posts']  // ה-query מספקת תגית 'Posts'
    }),

    addPost: builder.mutation({
        query: (post) => ({ url: '/posts', method: 'POST', body: post }),
        invalidatesTags: ['Posts']  // מבטלת את הקאש של 'Posts'
    })
})
// כשנקרא addPost, getPosts יתרענן אוטומטית!

Tags מתקדמים

endpoints: (builder) => ({
    getPosts: builder.query({
        query: () => '/posts',
        providesTags: (result) => 
            result
                ? [
                    ...result.map(({ id }) => ({ type: 'Post', id })),
                    { type: 'Post', id: 'LIST' }
                  ]
                : [{ type: 'Post', id: 'LIST' }]
    }),

    updatePost: builder.mutation({
        query: ({ id, ...patch }) => ({
            url: `/posts/${id}`,
            method: 'PATCH',
            body: patch
        }),
        // מבטל רק את הפוסט הספציפי
        invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }]
    })
})

📌 Optimistic Updates

בעדכון אופטימי מעדכנים את התצוגה מיד, עוד לפני שהשרת הספיק להשיב, כדי שהממשק ירגיש מהיר ותגובתי. אם השרת מחזיר שגיאה, מבטלים את השינוי וחוזרים למצב הקודם. RTK Query מספק לכך את onQueryStarted יחד עם undo:

updatePost: builder.mutation({
    query: ({ id, ...patch }) => ({
        url: `/posts/${id}`,
        method: 'PATCH',
        body: patch
    }),

    async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
        // עדכון אופטימיסטי - לפני התשובה מהשרת
        const patchResult = dispatch(
            postsApi.util.updateQueryData('getPosts', undefined, (draft) => {
                const post = draft.find(p => p.id === id);
                if (post) {
                    Object.assign(post, patch);
                }
            })
        );

        try {
            await queryFulfilled;
        } catch {
            // אם נכשל - חזור לערך הקודם
            patchResult.undo();
        }
    }
})

📋 סיכום

Hook סוג שימוש
useGetXQuery Query קריאת נתונים
useXMutation Mutation שינוי נתונים
providesTags - הגדרת תגיות קאש
invalidatesTags - ביטול קאש

תכונות אוטומטיות

  • ✅ Loading/Error states
  • ✅ Caching
  • ✅ Automatic refetch
  • ✅ Deduplication
  • ✅ Polling
  • ✅ Optimistic updates

הקודם: ← Redux Hooks
הבא: Testing →

🧪 Testing - React Testing Library & Jest

מבוא

בדיקות אוטומטיות מעניקות ביטחון שהקוד ממשיך לעבוד גם אחרי שינויים. שני כלים משלימים מובילים בעולם ה-React: Jest - סביבת הרצת הבדיקות (test runner) שמריצה את הבדיקות ומספקת פונקציות השוואה (assertions), ו-React Testing Library (RTL) - ספרייה שבודקת קומפוננטות מנקודת מבטו של המשתמש, כלומר לפי מה שמוצג על המסך ולא לפי פרטי המימוש הפנימיים.

התקנה

npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest

📌 מבנה בסיסי של בדיקה

// Component.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import MyComponent from './MyComponent';

describe('MyComponent', () => {
    test('renders correctly', () => {
        render(<MyComponent />);

        expect(screen.getByText('שלום')).toBeInTheDocument();
    });
});

📌 render ו-screen

import { render, screen } from '@testing-library/react';

test('renders button with text', () => {
    // render - מרנדר את הקומפוננטה
    render(<Button>לחץ כאן</Button>);

    // screen - גישה לאלמנטים שרונדרו
    const button = screen.getByRole('button', { name: /לחץ כאן/i });

    expect(button).toBeInTheDocument();
});

📌 Queries - חיפוש אלמנטים

סוגי Queries

Query מתי להשתמש
getBy... האלמנט חייב להיות קיים
queryBy... האלמנט עלול לא להיות קיים
findBy... אלמנט אסינכרוני (Promise)

שיטות חיפוש

// לפי Role (מומלץ!)
screen.getByRole('button');
screen.getByRole('heading', { level: 1 });
screen.getByRole('textbox', { name: /אימייל/i });

// לפי Label
screen.getByLabelText('סיסמה');

// לפי Placeholder
screen.getByPlaceholderText('הקלד כאן...');

// לפי טקסט
screen.getByText('שלום עולם');
screen.getByText(/שלום/i); // case-insensitive

// לפי Test ID
screen.getByTestId('submit-button');

// לפי Alt
screen.getByAltText('לוגו החברה');

דוגמאות

test('form elements', () => {
    render(<LoginForm />);

    // getBy - זורק שגיאה אם לא נמצא
    const emailInput = screen.getByLabelText(/אימייל/i);
    const passwordInput = screen.getByLabelText(/סיסמה/i);
    const submitButton = screen.getByRole('button', { name: /התחבר/i });

    // queryBy - מחזיר null אם לא נמצא
    const error = screen.queryByText(/שגיאה/i);
    expect(error).not.toBeInTheDocument();
});

📌 User Events - אינטראקציות

הספרייה user-event מדמה אינטראקציות אמיתיות של משתמש - הקלדה, לחיצה, בחירה מרשימה ועוד - בצורה קרובה יותר להתנהגות בדפדפן. מכיוון שהפעולות אסינכרוניות, מקדימים להן await:

import userEvent from '@testing-library/user-event';

test('user interactions', async () => {
    const user = userEvent.setup();
    render(<MyForm onSubmit={handleSubmit} />);

    // הקלדה
    await user.type(screen.getByLabelText(/שם/i), 'דני');

    // לחיצה
    await user.click(screen.getByRole('button'));

    // Clear + type
    await user.clear(screen.getByLabelText(/שם/i));
    await user.type(screen.getByLabelText(/שם/i), 'רונית');

    // Hover
    await user.hover(screen.getByText('עזרה'));

    // Select
    await user.selectOptions(screen.getByRole('combobox'), 'option1');

    // Checkbox
    await user.click(screen.getByRole('checkbox'));
});

📌 Assertions - בדיקות

import '@testing-library/jest-dom';

test('assertions', () => {
    render(<MyComponent />);

    const element = screen.getByText('שלום');

    // נוכחות
    expect(element).toBeInTheDocument();
    expect(screen.queryByText('לא קיים')).not.toBeInTheDocument();

    // תוכן
    expect(element).toHaveTextContent('שלום');
    expect(screen.getByLabelText('שם')).toHaveValue('דני');

    // מצב
    expect(screen.getByRole('button')).toBeEnabled();
    expect(screen.getByRole('button')).toBeDisabled();

    // Style
    expect(element).toHaveClass('active');
    expect(element).toHaveStyle({ color: 'red' });

    // Attributes
    expect(element).toHaveAttribute('href', '/about');
});

📝 דוגמאות מלאות

בדיקת קומפוננטה פשוטה

// Greeting.jsx
function Greeting({ name }) {
    return <h1>שלום, {name}!</h1>;
}

// Greeting.test.jsx
test('renders greeting with name', () => {
    render(<Greeting name="דני" />);

    expect(screen.getByRole('heading')).toHaveTextContent('שלום, דני!');
});

test('renders greeting with different name', () => {
    render(<Greeting name="רונית" />);

    expect(screen.getByText(/שלום, רונית/i)).toBeInTheDocument();
});

בדיקת Counter

// Counter.jsx
function Counter() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <p data-testid="count">{count}</p>
            <button onClick={() => setCount(c => c + 1)}>הוסף</button>
            <button onClick={() => setCount(c => c - 1)}>הפחת</button>
        </div>
    );
}

// Counter.test.jsx
describe('Counter', () => {
    test('starts at 0', () => {
        render(<Counter />);
        expect(screen.getByTestId('count')).toHaveTextContent('0');
    });

    test('increments on button click', async () => {
        const user = userEvent.setup();
        render(<Counter />);

        await user.click(screen.getByRole('button', { name: /הוסף/i }));

        expect(screen.getByTestId('count')).toHaveTextContent('1');
    });

    test('decrements on button click', async () => {
        const user = userEvent.setup();
        render(<Counter />);

        await user.click(screen.getByRole('button', { name: /הפחת/i }));

        expect(screen.getByTestId('count')).toHaveTextContent('-1');
    });
});

בדיקת טופס

// LoginForm.test.jsx
describe('LoginForm', () => {
    const mockSubmit = jest.fn();

    beforeEach(() => {
        mockSubmit.mockClear();
    });

    test('submits form with valid data', async () => {
        const user = userEvent.setup();
        render(<LoginForm onSubmit={mockSubmit} />);

        await user.type(screen.getByLabelText(/אימייל/i), '[email protected]');
        await user.type(screen.getByLabelText(/סיסמה/i), '123456');
        await user.click(screen.getByRole('button', { name: /התחבר/i }));

        expect(mockSubmit).toHaveBeenCalledWith({
            email: '[email protected]',
            password: '123456'
        });
    });

    test('shows error for invalid email', async () => {
        const user = userEvent.setup();
        render(<LoginForm onSubmit={mockSubmit} />);

        await user.type(screen.getByLabelText(/אימייל/i), 'invalid');
        await user.click(screen.getByRole('button', { name: /התחבר/i }));

        expect(screen.getByText(/אימייל לא תקין/i)).toBeInTheDocument();
        expect(mockSubmit).not.toHaveBeenCalled();
    });
});

📌 בדיקות אסינכרוניות

// async component
function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        fetch(`/api/users/${userId}`)
            .then(res => res.json())
            .then(data => {
                setUser(data);
                setLoading(false);
            });
    }, [userId]);

    if (loading) return <p>טוען...</p>;
    return <h1>{user.name}</h1>;
}

// test
test('loads and displays user', async () => {
    // Mock fetch
    global.fetch = jest.fn(() =>
        Promise.resolve({
            json: () => Promise.resolve({ name: 'דני' })
        })
    );

    render(<UserProfile userId="1" />);

    // בהתחלה מציג loading
    expect(screen.getByText(/טוען/i)).toBeInTheDocument();

    // מחכה לתוכן האסינכרוני
    const heading = await screen.findByRole('heading');
    expect(heading).toHaveTextContent('דני');
});

📌 Mocking

לעיתים קרובות נרצה לבודד את הקומפוננטה הנבדקת מהתלויות החיצוניות שלה - למשל קריאות לשרת או פונקציות שמועברות כ-props. לשם כך משתמשים ב-Mocks: תחליפים מזויפים שמאפשרים לשלוט בהתנהגות ולוודא שהקומפוננטה קראה להם כצפוי:

Mock Functions

test('calls onClick when clicked', async () => {
    const handleClick = jest.fn();
    const user = userEvent.setup();

    render(<Button onClick={handleClick}>לחץ</Button>);

    await user.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledTimes(1);
});

Mock Modules

// __mocks__/api.js
export const fetchUsers = jest.fn();

// Component.test.jsx
jest.mock('./api');
import { fetchUsers } from './api';

beforeEach(() => {
    fetchUsers.mockResolvedValue([
        { id: 1, name: 'דני' }
    ]);
});

test('displays fetched users', async () => {
    render(<UserList />);

    expect(await screen.findByText('דני')).toBeInTheDocument();
});

📋 Best Practices

  1. בדקו התנהגות, לא מימוש - התמקדו במה שהמשתמש רואה ועושה, ולא בפרטים הפנימיים של הקוד
  2. העדיפו חיפוש לפי Role - הוא עמיד יותר ומקדם נגישות, בניגוד ל-test-id
  3. כתבו בדיקות שמדמות שימוש אמיתי - בדיקה טובה משקפת את דרך העבודה של משתמש חי
  4. השתמשו ב-userEvent ולא ב-fireEvent - הוא מדמה אינטראקציות נאמנה יותר למציאות

📋 סיכום

API שימוש
render() רינדור קומפוננטה
screen גישה לאלמנטים
getBy/queryBy/findBy חיפוש אלמנטים
userEvent סימולציית אינטראקציות
expect() assertions
jest.fn() mock function

הקודם: ← RTK Query
סיום! 🎉

📱 React Native עם Expo - מדריך מקיף

מה זה React Native?

React Native הוא פריימוורק של Meta (Facebook) לפיתוח אפליקציות מובייל Native לאנדרואיד ול-iOS, באמצעות אותם JavaScript ו-React שכבר הכרנו. היתרון הגדול: במקום ללמוד שפות שונות ולתחזק שתי אפליקציות נפרדות, כותבים בסיס קוד אחד שמתורגם לרכיבי ממשק מקוריים בכל פלטפורמה - לא אתר עטוף (WebView), אלא אפליקציה אמיתית לכל דבר.

React (Web)         →  HTML Elements  →  דפדפן
React Native (Mobile) →  Native Components →  iOS / Android

יתרונות React Native

יתרון הסבר
קוד אחד אותו קוד לשתי הפלטפורמות
React אותו תחביר ועקרונות מ-React
Native UI אמיתי, לא WebView
Hot Reload רענון מהיר בפיתוח
קהילה גדולה הרבה ספריות וכלים
ביצועים קרוב ל-Native אמיתי

🚀 מה זה Expo?

Expo הוא מסגרת עבודה ומערכת כלים שנבנו סביב React Native כדי לפשט מאוד את הפיתוח. במקום להתמודד עם הגדרת סביבות מורכבות ב-Android Studio וב-Xcode, Expo מטפל בכל התשתית עבורכם - כך שאפשר להתחיל לפתח ולראות תוצאות על מכשיר אמיתי תוך דקות. זו נקודת הפתיחה המומלצת ביותר למי שמתחיל עם React Native.

השוואה: Expo vs React Native CLI

                    Expo              React Native CLI
                    ====              ================
התקנה:              קלה מאוד          מורכבת
זמן הקמה:           דקות             שעות
דרישות:             Node.js בלבד     Xcode, Android Studio
Native Modules:     מוגבל*           מלא
גודל APK:           גדול יותר        קטן יותר
למתחילים:           מומלץ מאוד       מאתגר

* Expo Go מוגבל, אבל EAS Build מאפשר הכל

סוגי Workflows ב-Expo

1. Managed Workflow (מנוהל)
   - Expo מנהל את הכל
   - אין גישה לקוד Native
   - הכי פשוט למתחילים

2. Bare Workflow (חשוף)
   - גישה מלאה לקוד Native
   - יותר שליטה
   - דומה ל-React Native CLI

🛠️ התקנה והגדרה

1. התקנת Node.js

# בדיקה שיש Node.js
node --version  # צריך 18+
npm --version

2. התקנת Expo CLI

# אין צורך להתקין גלובלית יותר, משתמשים ב-npx
npx create-expo-app@latest

3. יצירת פרויקט חדש

# יצירת פרויקט
npx create-expo-app@latest my-app

# או עם template
npx create-expo-app@latest my-app --template blank
npx create-expo-app@latest my-app --template blank-typescript
npx create-expo-app@latest my-app --template tabs

# כניסה לפרויקט
cd my-app

4. הרצת הפרויקט

# הרצה
npx expo start

# או
npm start

5. צפייה באפליקציה

  • Expo Go App: הורידו מה-App Store או Google Play
  • סרקו את ה-QR Code מהטרמינל
  • או לחצו a לאנדרואיד / i ל-iOS (דורש אמולטור)

📁 מבנה פרויקט Expo

my-app/
├── app/                    # App Router (Expo Router)
│   ├── (tabs)/            # קבוצת טאבים
│   │   ├── index.tsx      # מסך ראשי
│   │   └── explore.tsx    # מסך נוסף
│   ├── _layout.tsx        # Layout ראשי
│   └── +not-found.tsx     # דף 404
├── assets/                 # תמונות, פונטים, וכו'
│   ├── images/
│   └── fonts/
├── components/             # רכיבים משותפים
├── constants/              # קבועים
├── hooks/                  # Custom Hooks
├── app.json               # הגדרות האפליקציה
├── package.json           # תלויות
├── tsconfig.json          # הגדרות TypeScript
└── babel.config.js        # הגדרות Babel

🧩 רכיבים בסיסיים

ב-React Native אין תגיות HTML כמו div או p. במקומן משתמשים ברכיבים מובנים שמתורגמים לרכיבי ממשק מקוריים. אלו אבני הבניין שתפגשו כמעט בכל מסך:

View - הקונטיינר הבסיסי (כמו div)

import { View, StyleSheet } from 'react-native';

export default function MyComponent() {
  return (
    <View style={styles.container}>
      {/* תוכן */}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    padding: 20,
  },
});

Text - טקסט (חובה לכל טקסט!)

import { Text, View } from 'react-native';

export default function TextExample() {
  return (
    <View>
      {/* ❌ לא עובד ב-React Native */}
      {/* <View>שלום עולם</View> */}

      {/* ✅ נכון */}
      <Text>שלום עולם!</Text>

      <Text style={{ fontSize: 24, fontWeight: 'bold', color: 'blue' }}>
        כותרת גדולה
      </Text>

      {/* Text מקונן */}
      <Text>
        טקסט רגיל <Text style={{ fontWeight: 'bold' }}>מודגש</Text> ועוד
      </Text>
    </View>
  );
}

Image - תמונות

import { Image, View } from 'react-native';

export default function ImageExample() {
  return (
    <View>
      {/* תמונה מקומית */}
      <Image 
        source={require('./assets/logo.png')}
        style={{ width: 100, height: 100 }}
      />

      {/* תמונה מה-URL */}
      <Image
        source={{ uri: 'https://example.com/image.jpg' }}
        style={{ width: 200, height: 200 }}
        resizeMode="cover"  // cover, contain, stretch, center
      />
    </View>
  );
}

Button ו-Pressable

import { Button, Pressable, Text, View, Alert } from 'react-native';

export default function ButtonExample() {
  const handlePress = () => {
    Alert.alert('לחצת!', 'הכפתור עובד');
  };

  return (
    <View>
      {/* Button בסיסי (מוגבל בסגנון) */}
      <Button 
        title="לחץ כאן" 
        onPress={handlePress}
        color="#841584"
      />

      {/* Pressable - יותר גמיש */}
      <Pressable
        onPress={handlePress}
        style={({ pressed }) => [
          {
            backgroundColor: pressed ? '#ddd' : '#007AFF',
            padding: 15,
            borderRadius: 10,
            marginTop: 10,
          }
        ]}
      >
        <Text style={{ color: 'white', textAlign: 'center' }}>
          כפתור מותאם
        </Text>
      </Pressable>
    </View>
  );
}

TextInput - שדה קלט

import { useState } from 'react';
import { TextInput, View, Text, StyleSheet } from 'react-native';

export default function InputExample() {
  const [text, setText] = useState('');
  const [password, setPassword] = useState('');

  return (
    <View style={styles.container}>
      {/* שדה טקסט רגיל */}
      <TextInput
        style={styles.input}
        placeholder="הכנס שם"
        value={text}
        onChangeText={setText}
      />

      {/* שדה סיסמה */}
      <TextInput
        style={styles.input}
        placeholder="סיסמה"
        value={password}
        onChangeText={setPassword}
        secureTextEntry={true}
      />

      {/* שדה מספרי */}
      <TextInput
        style={styles.input}
        placeholder="מספר טלפון"
        keyboardType="phone-pad"
      />

      {/* שדה אימייל */}
      <TextInput
        style={styles.input}
        placeholder="אימייל"
        keyboardType="email-address"
        autoCapitalize="none"
      />

      <Text>הקלדת: {text}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { padding: 20 },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 8,
    padding: 12,
    marginBottom: 10,
    fontSize: 16,
  },
});

ScrollView - גלילה

import { ScrollView, View, Text, StyleSheet } from 'react-native';

export default function ScrollExample() {
  return (
    <ScrollView style={styles.container}>
      {[...Array(20)].map((_, i) => (
        <View key={i} style={styles.item}>
          <Text>פריט מספר {i + 1}</Text>
        </View>
      ))}
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  item: {
    padding: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#eee',
  },
});

FlatList - רשימה (יעיל לרשימות ארוכות!)

import { FlatList, View, Text, StyleSheet } from 'react-native';

const DATA = [
  { id: '1', title: 'פריט ראשון' },
  { id: '2', title: 'פריט שני' },
  { id: '3', title: 'פריט שלישי' },
  // ... עוד הרבה פריטים
];

export default function ListExample() {
  const renderItem = ({ item }) => (
    <View style={styles.item}>
      <Text>{item.title}</Text>
    </View>
  );

  return (
    <FlatList
      data={DATA}
      renderItem={renderItem}
      keyExtractor={item => item.id}
      ItemSeparatorComponent={() => <View style={styles.separator} />}
      ListHeaderComponent={() => <Text style={styles.header}>הרשימה שלי</Text>}
      ListEmptyComponent={() => <Text>אין פריטים</Text>}
    />
  );
}

const styles = StyleSheet.create({
  item: { padding: 20 },
  separator: { height: 1, backgroundColor: '#eee' },
  header: { fontSize: 24, padding: 20, fontWeight: 'bold' },
});

🎨 עיצוב ב-React Native

העיצוב ב-React Native דומה ל-CSS אך אינו זהה לו: כותבים אותו כאובייקטים של JavaScript (בדרך כלל דרך StyleSheet.create), שמות המאפיינים ב-camelCase, והפריסה מבוססת כולה על Flexbox. שימו לב לכמה הבדלים מהותיים: הערכים הם מספרים ולא מחרוזות עם px, ו-flexDirection הוא column כברירת מחדל (בניגוד ל-row בדפדפן):

StyleSheet

import { StyleSheet, View, Text } from 'react-native';

export default function StyledComponent() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>כותרת</Text>
      <Text style={[styles.text, styles.highlight]}>טקסט מודגש</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
    padding: 20,
    // אין margin: auto, אין display: grid
    // משתמשים ב-Flexbox!
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 10,
  },
  text: {
    fontSize: 16,
    color: '#666',
    lineHeight: 24,
  },
  highlight: {
    backgroundColor: 'yellow',
    padding: 5,
  },
});

Flexbox (ברירת המחדל!)

const styles = StyleSheet.create({
  // עמודה (ברירת מחדל - שונה מ-Web!)
  column: {
    flexDirection: 'column',  // ברירת מחדל
  },

  // שורה
  row: {
    flexDirection: 'row',
  },

  // מרכוז
  centered: {
    justifyContent: 'center',  // ציר ראשי
    alignItems: 'center',      // ציר משני
  },

  // חלוקת מרחב
  spaceBetween: {
    justifyContent: 'space-between',
  },

  // גמישות
  flexible: {
    flex: 1,  // תופס את כל המקום הזמין
  },
});

הבדלים מ-CSS

// ❌ לא עובד ב-React Native
// margin: '10px'
// font-size: 16px
// background-color: red

// ✅ נכון ב-React Native
{
  margin: 10,           // מספר בלבד (pixels)
  fontSize: 16,         // camelCase
  backgroundColor: 'red',

  // ❌ אין:
  // display: 'block' / 'inline'
  // float
  // position: 'fixed'

  // ✅ יש:
  // position: 'absolute' / 'relative'
  // flexbox (ברירת מחדל)
}

🧭 ניווט עם Expo Router

Expo Router מיישם ניתוב מבוסס קבצים (File-based routing), בדומה ל-Next.js: מבנה התיקיות בתוך תיקיית app הוא שמגדיר את הנתיבים באפליקציה. כך אין צורך להגדיר נתיבים ידנית - כל קובץ הופך אוטומטית למסך:

התקנה

npx expo install expo-router expo-linking expo-constants expo-status-bar

מבנה תיקיות

app/
├── _layout.tsx        # Layout ראשי
├── index.tsx          # דף הבית (/)
├── about.tsx          # דף אודות (/about)
├── profile/
│   ├── index.tsx      # /profile
│   └── [id].tsx       # /profile/123 (דינמי)
└── (tabs)/            # קבוצת טאבים
    ├── _layout.tsx    # Layout לטאבים
    ├── home.tsx       # Tab 1
    └── settings.tsx   # Tab 2

Layout בסיסי

// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen 
        name="index" 
        options={{ title: 'דף הבית' }} 
      />
      <Stack.Screen 
        name="about" 
        options={{ title: 'אודות' }} 
      />
    </Stack>
  );
}

ניווט בין מסכים

// app/index.tsx
import { Link, router } from 'expo-router';
import { View, Text, Pressable } from 'react-native';

export default function HomeScreen() {
  return (
    <View>
      {/* Link (דומה לאנכור) */}
      <Link href="/about">
        <Text>עבור לאודות</Text>
      </Link>

      {/* ניווט פרוגרמטי */}
      <Pressable onPress={() => router.push('/profile/123')}>
        <Text>עבור לפרופיל</Text>
      </Pressable>

      {/* חזרה */}
      <Pressable onPress={() => router.back()}>
        <Text>חזור</Text>
      </Pressable>
    </View>
  );
}

קבלת פרמטרים

// app/profile/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';

export default function ProfileScreen() {
  const { id } = useLocalSearchParams();

  return (
    <View>
      <Text>פרופיל מספר: {id}</Text>
    </View>
  );
}

Tab Navigation

// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen
        name="home"
        options={{
          title: 'בית',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="settings"
        options={{
          title: 'הגדרות',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="settings" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

📦 APIs נפוצים של Expo

אחד היתרונות הגדולים של Expo הוא אוסף המודולים המובנים לגישה ליכולות המכשיר - אחסון, מצלמה, מיקום, התראות ועוד. כל אחד מהם מותקן בפקודה אחת ומספק API נקי ואחיד לשתי הפלטפורמות. הנה כמה מהנפוצים ביותר:

AsyncStorage - אחסון מקומי

npx expo install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';

// שמירה
await AsyncStorage.setItem('user', JSON.stringify({ name: 'דוד' }));

// קריאה
const userJson = await AsyncStorage.getItem('user');
const user = userJson ? JSON.parse(userJson) : null;

// מחיקה
await AsyncStorage.removeItem('user');

SecureStore - אחסון מאובטח

npx expo install expo-secure-store
import * as SecureStore from 'expo-secure-store';

// שמירה
await SecureStore.setItemAsync('token', 'abc123');

// קריאה
const token = await SecureStore.getItemAsync('token');

// מחיקה
await SecureStore.deleteItemAsync('token');

Camera - מצלמה

npx expo install expo-camera
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useState } from 'react';
import { View, Button, Text } from 'react-native';

export default function CameraExample() {
  const [permission, requestPermission] = useCameraPermissions();

  if (!permission) {
    return <View />;
  }

  if (!permission.granted) {
    return (
      <View>
        <Text>צריך הרשאה למצלמה</Text>
        <Button onPress={requestPermission} title="תן הרשאה" />
      </View>
    );
  }

  return (
    <CameraView style={{ flex: 1 }} facing="back">
      {/* כפתורי שליטה */}
    </CameraView>
  );
}

Location - מיקום

npx expo install expo-location
import * as Location from 'expo-location';
import { useEffect, useState } from 'react';
import { View, Text } from 'react-native';

export default function LocationExample() {
  const [location, setLocation] = useState(null);

  useEffect(() => {
    (async () => {
      const { status } = await Location.requestForegroundPermissionsAsync();
      if (status !== 'granted') {
        return;
      }

      const loc = await Location.getCurrentPositionAsync({});
      setLocation(loc);
    })();
  }, []);

  return (
    <View>
      {location && (
        <Text>
          מיקום: {location.coords.latitude}, {location.coords.longitude}
        </Text>
      )}
    </View>
  );
}

Notifications - התראות

npx expo install expo-notifications expo-device
import * as Notifications from 'expo-notifications';
import { useEffect } from 'react';

// הגדרת התנהגות כשמתקבלת התראה
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});

export default function NotificationExample() {
  useEffect(() => {
    registerForPushNotificationsAsync();
  }, []);

  // שליחת התראה מקומית
  const sendLocalNotification = async () => {
    await Notifications.scheduleNotificationAsync({
      content: {
        title: "שלום!",
        body: "זו התראה מהאפליקציה",
      },
      trigger: { seconds: 2 },
    });
  };

  return (/* ... */);
}

🏗️ בניה והפצה

כשהאפליקציה מוכנה, יש להפוך אותה לקובץ התקנה (APK או IPA) ולהעלות אותה לחנויות. Expo מספק לכך שירות ענן בשם EAS (Expo Application Services), שבונה את האפליקציה עבורכם ללא צורך ב-Xcode או Android Studio מקומיים:

EAS Build - בניית אפליקציה

# התקנת EAS CLI
npm install -g eas-cli

# התחברות
eas login

# הגדרה ראשונית
eas build:configure

# בנייה לאנדרואיד
eas build --platform android

# בנייה ל-iOS
eas build --platform ios

# בנייה לשניהם
eas build --platform all

סוגי Build

# Development build (לפיתוח)
eas build --profile development --platform android

# Preview build (לבדיקות)
eas build --profile preview --platform android

# Production build (להפצה)
eas build --profile production --platform android

EAS Submit - העלאה לחנויות

# העלאה ל-Google Play
eas submit --platform android

# העלאה ל-App Store
eas submit --platform ios

📝 דוגמה מלאה - אפליקציית Todo

// app/index.tsx
import { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  FlatList,
  Pressable,
  StyleSheet,
} from 'react-native';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

export default function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [inputText, setInputText] = useState('');

  const addTodo = () => {
    if (inputText.trim()) {
      setTodos([
        ...todos,
        { id: Date.now().toString(), text: inputText, completed: false },
      ]);
      setInputText('');
    }
  };

  const toggleTodo = (id: string) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const deleteTodo = (id: string) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>📝 המשימות שלי</Text>

      <View style={styles.inputContainer}>
        <TextInput
          style={styles.input}
          placeholder="הוסף משימה..."
          value={inputText}
          onChangeText={setInputText}
          onSubmitEditing={addTodo}
        />
        <Pressable style={styles.addButton} onPress={addTodo}>
          <Text style={styles.addButtonText}>+</Text>
        </Pressable>
      </View>

      <FlatList
        data={todos}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={styles.todoItem}>
            <Pressable
              style={styles.checkbox}
              onPress={() => toggleTodo(item.id)}
            >
              <Text>{item.completed ? '✅' : '⬜'}</Text>
            </Pressable>
            <Text
              style={[
                styles.todoText,
                item.completed && styles.completedText,
              ]}
            >
              {item.text}
            </Text>
            <Pressable onPress={() => deleteTodo(item.id)}>
              <Text style={styles.deleteButton}>🗑️</Text>
            </Pressable>
          </View>
        )}
        ListEmptyComponent={
          <Text style={styles.emptyText}>אין משימות. הוסף אחת!</Text>
        }
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
    padding: 20,
    paddingTop: 60,
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 20,
  },
  inputContainer: {
    flexDirection: 'row',
    marginBottom: 20,
  },
  input: {
    flex: 1,
    backgroundColor: 'white',
    padding: 15,
    borderRadius: 10,
    fontSize: 16,
    marginRight: 10,
  },
  addButton: {
    backgroundColor: '#007AFF',
    width: 50,
    borderRadius: 10,
    justifyContent: 'center',
    alignItems: 'center',
  },
  addButtonText: {
    color: 'white',
    fontSize: 24,
    fontWeight: 'bold',
  },
  todoItem: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: 'white',
    padding: 15,
    borderRadius: 10,
    marginBottom: 10,
  },
  checkbox: {
    marginRight: 10,
  },
  todoText: {
    flex: 1,
    fontSize: 16,
  },
  completedText: {
    textDecorationLine: 'line-through',
    color: '#888',
  },
  deleteButton: {
    fontSize: 20,
  },
  emptyText: {
    textAlign: 'center',
    color: '#888',
    marginTop: 50,
  },
});

📚 משאבים נוספים


📝 סיכום

נושא טכנולוגיה
פלטפורמה Expo
ניווט Expo Router
סגנונות StyleSheet + Flexbox
אחסון AsyncStorage / SecureStore
בנייה EAS Build
הפצה EAS Submit

בהצלחה! 🎉