מפתח

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

קורס מקיף ללימוד React מאפס עד רמה מתקדמת. הקורס מכסה את כל הנושאים החשובים: מיסודות ועד Redux ו-Testing.

למה 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), שפותחה על ידי Facebook ושוחררה בפעם הראשונה בשנת 2013. React מתמקדת ביצירת קומפוננטות (רכיבים) שניתן להרכיב יחד לממשק מורכב.

🎯 למה נוצרה React?

Facebook נתקלו בבעיות עם ממשקים מורכבים:

// בעיה: עדכון ידני של ה-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 משתמשת ב-Virtual DOM - עותק וירטואלי של ה-DOM:

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

זה הרבה יותר מהיר מעדכון ישיר של ה-DOM!

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

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

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

3. One-Way Data Flow

הנתונים זורמים בכיוון אחד - מההורה לילד:

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?

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

📌 סיכום

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

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


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

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

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

1. Node.js

הורידו והתקינו Node.js מהאתר הרשמי: nodejs.org

בדקו שההתקנה הצליחה:

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

2. עורך קוד

מומלץ: Visual Studio Code עם התוספים: - ES7+ React/Redux/React-Native snippets - Prettier - Code formatter - 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 מהיר יותר ומומלץ לפרויקטים חדשים


📁 מבנה הפרויקט (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> התקנת חבילה

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

# ניווט (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
const element = <h1>שלום עולם!</h1>;

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

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

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

כל קומפוננטה חייבת להחזיר אלמנט אחד בלבד:

// ❌ שגוי - שני אלמנטים שורש
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 לתכונות

תכונות HTML נכתבות ב-camelCase:

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

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

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

סוגריים מסולסלים {} מאפשרים להכניס JavaScript לתוך JSX:

משתנים

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

Fragments מאפשרים לקבץ אלמנטים בלי להוסיף צומת DOM נוסף:

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

// ❌ if/else רגיל - לא עובד
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)

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

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

// קומפוננטה = פונקציה שמחזירה 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.


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

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

// 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

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

קומפוננטות יכולות להכיל קומפוננטות אחרות:

// קומפוננטות קטנות
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 - קומפוננטה סטטית
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:

שיטה 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

אי אפשר לשנות 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="לוגו" />;
}

💡 טיפ: אם מעבירים props דרך הרבה רמות, שקלו להשתמש ב-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 הוא נתון פנימי של קומפוננטה שיכול להשתנות לאורך זמן. כשה-State משתנה - הקומפוננטה מתרנדרת מחדש!

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 הוא אסינכרוני

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 - מעבירים פונקציה ולא מחרוזת

// 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 - מונע התנהגות ברירת מחדל
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 הם פונקציות מיוחדות שמאפשרות לנו להשתמש ב-State ופיצ'רים אחרים של React בתוך Function Components. הם נוספו ב-React 16.8 (2019).

import { useState, useEffect } from 'react';

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

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

📌 חוקי Hooks

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

// ✅ נכון - ברמה העליונה
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

כבר למדנו! תזכורת מהירה:

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

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

🟢 useEffect

מאפשר לבצע side effects כמו: - קריאות API - Subscriptions - שינוי DOM ידני - טיימרים

התחביר

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

מאפשר לשמור ערך שלא גורם לרינדור מחדש, או להחזיק reference ל-DOM element.

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

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

שומר reference לפונקציה - שימושי כשמעבירים callbacks לילדים.

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

שיתוף נתונים בין קומפוננטות ללא prop drilling.

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 משלנו לשימוש חוזר בלוגיקה:

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>

⚠️ זהירות: אם הערך הוא 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() כדי להציג רשימות של נתונים.

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

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

🔑 חשיבות ה-key

key הוא מזהה ייחודי שעוזר ל-React לזהות איזה פריט השתנה, נוסף, או נמחק.

✅ נכון - 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)

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

📝 תרגילים

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


🟢 רמה בסיסית

תרגיל 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` ו-`useRef` לשמירת ה-interval.

🔴 רמה מתקדמת

תרגיל 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

מבוא

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

ב-Function Components, אנחנו משתמשים ב-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 - דליפות זיכרון

דליפות זיכרון קורות כש: - טיימרים לא נעצרים - 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


📌 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 לקומפוננטה ההורה המשותפת הקרובה ביותר.

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

// ❌ בעיה - כל קומפוננטה עם 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) היא כזו שה-State שלה מנוהל על ידי ההורה:

// ✅ 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 Functions
  3. Siblings: Lifting State Up
  4. Deep/Global: Context API / State Management

📌 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

דפוס מתקדם לתקשורת מורכבת (בלי Context):

// פשוט יותר להשתמש ב-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 באופן מובנה
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 דרך 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 פותר את בעיית ה-Props Drilling. הוא מאפשר להעביר נתונים לכל קומפוננטה בעץ, בלי להעביר props דרך כל רמה.

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

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

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 גורם לרינדור מחדש של כל הצרכנים כשהערך משתנה!

בעיה

// ❌ כל שינוי ב-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 (סגנון ישן)

דרך ישנה יותר לצרוך Context (עדיין עובד):

// סגנון ישן עם 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 אחרים. הוא מאפשר לחלץ לוגיקה משותפת לשימוש חוזר בין קומפוננטות.

// במקום לשכפל קוד...
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)

מבוא

React Patterns הם דפוסי עיצוב נפוצים לפתרון בעיות חוזרות. נלמד על HOC, Render Props, Error Boundaries, ו-Fragments.


📌 HOC - Higher Order Component

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 ומזינה אותה עם נתונים.

התחביר

// קומפוננטה עם 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 בילדים שלה ומציגה UI חלופי.

⚠️ Error Boundaries חייבים להיות Class Components!

התחביר

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 מאפשר לקבץ אלמנטים בלי להוסיף צומת DOM נוסף.

תחביר

// תחביר מלא
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 Router היא ספרייה לניתוב (Routing) באפליקציות React. היא מאפשרת יצירת אפליקציות SPA (Single Page Application) עם מספר "דפים".

התקנה

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 (נתיבים דינמיים)

הגדרת פרמטרים

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 (נתיבים מקוננים)

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

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

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

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 לא השתנו.

הבעיה

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

useMemo שומר תוצאה של חישוב ומחשב מחדש רק כשהתלויות משתנות.

מתי להשתמש?

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

למה זה חשוב?

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 מציג מצב טעינה בזמן ש-lazy components נטענים.


📌 React.lazy

React.lazy מאפשר לטעון קומפוננטות בצורה דינמית (dynamic import).

התחביר

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 exports. לnamed exports:

// ❌ לא עובד ישירות
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 מורכב. הוא דומה ל-useState, אבל מתאים יותר כשיש: - State עם מספר ערכים קשורים - לוגיקת עדכון מורכבת - עדכונים שתלויים בערך הקודם


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

const [state, dispatch] = useReducer(reducer, initialState);

הרכיבים:

  1. state - המצב הנוכחי
  2. dispatch - פונקציה לשליחת פעולות (actions)
  3. reducer - פונקציה שמחשבת את ה-state החדש
  4. initialState - ערך התחלתי

📌 Reducer Function

פונקציית ה-reducer מקבלת state ו-action ומחזירה 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

שילוב נפוץ לניהול state גלובלי:

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 גלובלי לאפליקציות JavaScript. Redux Toolkit (RTK) הוא הדרך הרשמית והמומלצת להשתמש ב-Redux - פשוט וקל יותר!

התקנה

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 מכיל את ה-reducer, actions, ו-initial 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

עטיפת האפליקציה ב-Provider:

// 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

מבוא

פרק זה מכסה: - useSelector ו-useDispatch לעומק - Async actions עם createAsyncThunk - Middleware - דפוסים מתקדמים


📌 useSelector - קריאה מה-Store

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

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 - פעולות אסינכרוניות

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 מאפשר ללכוד actions לפני שהם מגיעים ל-reducer:

// 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. הוא מפשט מאוד קריאות API ומטפל אוטומטית ב: - Loading/Error states - Caching - Re-fetching - Optimistic updates


📌 הגדרת API

// 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)

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)

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 מאפשרים לקשר בין 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

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

מבוא

Jest הוא framework לבדיקות JavaScript. 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 - אינטראקציות

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

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. בדקו התנהגות, לא implementation
  2. השתמשו ב-roles במקום test-ids
  3. Avoid testing implementation details
  4. כתבו בדיקות שדומות לשימוש המשתמש
  5. השתמשו ב-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.

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 מטפל בכל זה עבורך!

השוואה: 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

🧩 רכיבים בסיסיים

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

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.

התקנה

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

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 (/* ... */);
}

🏗️ בניה והפצה

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

בהצלחה! 🎉