מפתח
ברוכים הבאים!
קורס מקיף ללימוד 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-2: יסודות (פרקים 1-11)
- שבוע 3: Lifecycle & Component Patterns (פרקים 12-14)
- שבוע 4: Context & Custom Hooks (פרקים 15-17)
- שבוע 5: Routing & Performance (פרקים 18-21)
- שבוע 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) | קהילה | |
| שוק עבודה | 🥇 הכי נפוץ | 🥈 | 🥉 |
💻 מי משתמש ב-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>
);
}
✅ כללים לשמות קומפוננטות
-
PascalCase - אות גדולה בתחילת כל מילה:
// ✅ נכון function UserProfile() { } function ShoppingCart() { } // ❌ שגוי function userProfile() { } function shopping_cart() { } -
שם תיאורי - מתאר את מה שהקומפוננטה עושה:
// ✅ נכון function ProductCard() { } function LoginForm() { } // ❌ לא ברור function Card1() { } function Component() { } -
קובץ = שם הקומפוננטה:
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(...)) |
🎯 אירועים (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 |
איבוד פוקוס |
🪝 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 |
הסתרה מלאה |
📋 רשימות (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
- חייב להיות ייחודי בין אחים
- חייב להיות יציב - לא להשתנות בין רינדורים
- לא להשתמש ב-
Math.random()אוDate.now() - 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', '');
// הערך נשמר גם אחרי רענון הדף!
📌 טיפים לפתרון
- התחילו פשוט - קודם גרמו לזה לעבוד, אחר כך שפרו
- חלקו לקומפוננטות - כל חלק נפרד בקומפוננטה משלו
- בדקו תוך כדי - הריצו ובדקו אחרי כל שינוי קטן
- קראו את השגיאות - הן מספרות בדיוק מה הבעיה
✅ פתרונות
הפתרונות נמצאים בתיקיית 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:
- קוד מסורבל וקשה לתחזוקה
- קומפוננטות ביניים "מזוהמות" ב-props לא רלוונטיים
- קשה לשנות את מבנה הנתונים
🔝 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
מבוא
קומפוננטות צריכות לתקשר זו עם זו. יש מספר דפוסים לתקשורת בין קומפוננטות:
- Parent → Child: Props
- Child → Parent: Callback Functions
- Siblings: Lifting State Up
- 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
- שם מתחיל ב-
use- חובה! (useMyHook, useFetch, useAuth) - קורא ל-Hooks אחרים - useState, useEffect, וכו'
- מחזיר ערכים - מה שהקומפוננטה צריכה
- לוגיקה טהורה - ללא 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>;
}
🔗 Link ו-NavLink
Link - קישור בסיסי
import { Link } from 'react-router-dom';
function Navigation() {
return (
<nav>
<Link to="/">בית</Link>
<Link to="/products">מוצרים</Link>
<Link to="/about">אודות</Link>
</nav>
);
}
NavLink - קישור עם סטייל פעיל
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);
הרכיבים:
- state - המצב הנוכחי
- dispatch - פונקציה לשליחת פעולות (actions)
- reducer - פונקציה שמחשבת את ה-state החדש
- 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
- בדקו התנהגות, לא implementation
- השתמשו ב-roles במקום test-ids
- Avoid testing implementation details
- כתבו בדיקות שדומות לשימוש המשתמש
- השתמשו ב-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 |
בהצלחה! 🎉