מפתח
ברוכים הבאים!
קורס מקיף שילווה אתכם ב-React מהצעד הראשון ועד רמה מתקדמת. הוא בנוי כמסלול רציף שמתחיל בעקרונות היסוד וממשיך עד לנושאים המתקדמים ביותר - ניהול מצב עם Redux, בדיקות (Testing) ופיתוח מובייל עם React Native.
למה React?
- 🌍 הספרייה הנפוצה בעולם לבניית ממשקי משתמש
- 🔄 Virtual DOM שמאפשר עדכוני מסך מהירים ויעילים
- 📦 קומפוננטות מודולריות שאפשר לעשות בהן שימוש חוזר
- 💪 מערכת כלים עשירה וקהילת מפתחים עצומה
📚 מבנה הקורס
חלק א' - יסודות
| מספר | נושא | קובץ |
|---|---|---|
| 1 | רקע והיסטוריה | 01_background.md |
| 2 | התקנה והגדרות | 02_installation.md |
| 3 | תחביר JSX | 03_jsx_syntax.md |
| 4 | קומפוננטות | 04_components.md |
| 5 | Props | 05_props.md |
| 6 | State | 06_state.md |
| 7 | אירועים | 07_events.md |
| 8 | Hooks בסיסיים | 08_hooks.md |
| 9 | רינדור מותנה | 09_conditional.md |
| 10 | רשימות | 10_lists.md |
| 11 | תרגילים | 11_exercises.md |
חלק ב' - Lifecycle & Patterns
| מספר | נושא | קובץ |
|---|---|---|
| 12 | Lifecycle & useEffect | 12_lifecycle.md |
| 13 | Props Drilling & Lifting State | 13_props_drilling.md |
| 14 | Component Communication | 14_component_patterns.md |
חלק ג' - Context & Hooks מתקדמים
| מספר | נושא | קובץ |
|---|---|---|
| 15 | Context API | 15_context.md |
| 16 | Custom Hooks | 16_custom_hooks.md |
| 17 | React Patterns (HOC, Error Boundaries) | 17_patterns.md |
חלק ד' - Routing & Performance
| מספר | נושא | קובץ |
|---|---|---|
| 18 | React Router | 18_routing.md |
| 19 | Performance (memo, useMemo, useCallback) | 19_performance.md |
| 20 | Lazy Loading & Suspense | 20_lazy_loading.md |
| 21 | useReducer | 21_useReducer.md |
חלק ה' - Redux & Testing
| מספר | נושא | קובץ |
|---|---|---|
| 22 | Redux Toolkit Basics | 22_redux.md |
| 23 | Redux Hooks & Async | 23_redux_hooks.md |
| 24 | RTK Query | 24_rtk_query.md |
| 25 | Testing (Jest & RTL) | 25_testing.md |
חלק ו' - React Native
| מספר | נושא | קובץ |
|---|---|---|
| 26 | React Native עם Expo | 26_react_native_expo.md |
📁 תיקיית דוגמאות
| מספר | נושא | קובץ |
|---|---|---|
| 1 | קומפוננטה ראשונה | 01_hello_component.jsx |
| 2 | Props | 02_props_example.jsx |
| 3 | State | 03_state_example.jsx |
| 4 | אירועים | 04_events_example.jsx |
| 5 | Hooks | 05_hooks_example.jsx |
| 6 | רינדור מותנה | 06_conditional_example.jsx |
| 7 | רשימות | 07_lists_example.jsx |
| 8 | Todo App | 08_todo_app.jsx |
| 9 | Lifecycle | 09_lifecycle_example.jsx |
| 10 | Lifting State | 10_lifting_state_example.jsx |
| 11 | Context API | 11_context_example.jsx |
| 12 | Custom Hooks | 12_custom_hooks_example.jsx |
| 13 | React Patterns | 13_patterns_example.jsx |
| 14 | useReducer | 14_useReducer_example.jsx |
| 15 | Performance | 15_performance_example.jsx |
| 16 | Redux Toolkit | 16_redux_example.jsx |
| 17 | React Native | 17_react_native_example.jsx |
🚀 איך להתחיל?
יצירת פרויקט React חדש
# עם Vite (מומלץ)
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
התקנת חבילות נוספות
# React Router
npm install react-router-dom
# Redux Toolkit
npm install @reduxjs/toolkit react-redux
# Testing
npm install --save-dev @testing-library/react @testing-library/jest-dom
📖 סדר לימוד מומלץ
- שבוע 1-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). היא פותחה בפייסבוק ושוחררה לציבור לראשונה בשנת 2013. הרעיון המרכזי שמאחוריה פשוט: לבנות את הממשק מיחידות קטנות ועצמאיות שנקראות קומפוננטות, ולהרכיב אותן יחד לממשק שלם ומורכב.
🎯 למה נוצרה React?
הצורך ב-React נולד מתוך כאב אמיתי. ככל שהממשקים בפייסבוק גדלו והפכו מורכבים יותר, ניהול ה-DOM באופן ידני הפך למסורבל ורגיש לשגיאות:
// בעיה: עדכון ידני של ה-DOM
document.getElementById('counter').innerHTML = newValue;
document.getElementById('total').innerHTML = calculateTotal();
document.getElementById('status').className = getStatusClass();
// ... וכן הלאה - קוד מסורבל ונפרד!
// React: הכל מתעדכן אוטומטית!
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
📅 ציר זמן
| שנה | אירוע |
|---|---|
| 2011 | פיתוח פנימי ב-Facebook |
| 2013 | שחרור React כקוד פתוח |
| 2015 | React Native לאפליקציות מובייל |
| 2016 | React 15 - שיפורי ביצועים |
| 2019 | React Hooks (גרסה 16.8) |
| 2022 | React 18 - Concurrent Features |
| 2024 | React 19 - Server Components |
🌟 יתרונות מרכזיים
1. Virtual DOM
React מנהלת עותק וירטואלי של ה-DOM שנקרא Virtual DOM. במקום לגעת בעמוד עצמו בכל שינוי, היא מעדכנת תחילה את העותק הזה:
שינוי ב-State ──► Virtual DOM מתעדכן ──► השוואה ──► רק השינויים מעודכנים ב-DOM האמיתי
הגישה הזו חוסכת עדכונים מיותרים ומהירה בהרבה מעדכון ישיר של ה-DOM.
2. קומפוננטות (Components)
כל חלק בממשק הוא קומפוננטה עצמאית, שאחראית על מקטע מוגדר של התצוגה:
function App() {
return (
<div>
<Header />
<Sidebar />
<MainContent />
<Footer />
</div>
);
}
3. One-Way Data Flow
הנתונים ב-React זורמים בכיוון אחד בלבד - מקומפוננטת ההורה אל קומפוננטות הילד. זרימה חד-כיוונית זו מקלה על מעקב אחר מקור הנתונים ועל איתור תקלות:
App (state) ──► Header (props) ──► Logo (props)
4. JSX
תחביר שמאפשר לשלב קוד JavaScript ותגי HTML באותו הקובץ:
const element = <h1>שלום עולם!</h1>;
const dynamic = <p>היום הוא {new Date().toLocaleDateString('he-IL')}</p>;
🔗 React מול ספריות אחרות
| תכונה | React | Vue | Angular |
|---|---|---|---|
| סוג | ספרייה | Framework | Framework |
| עקומת למידה | בינונית | קלה | תלולה |
| גודל (gzip) | ~40KB | ~33KB | ~143KB |
| שפה | JavaScript/JSX | JavaScript/Vue | TypeScript |
| גיבוי | Meta (Facebook) | קהילה | |
| שוק עבודה | 🥇 הכי נפוץ | 🥈 | 🥉 |
💻 מי משתמש ב-React?
כמעט כל חברת טכנולוגיה גדולה בונה חלק מהמוצרים שלה ב-React. הנה כמה דוגמאות מוכרות:
- Facebook / Instagram - היוצרים!
- Netflix - ממשק המשתמש
- Airbnb - אתר ואפליקציה
- WhatsApp Web - גרסת הווב
- Dropbox - ממשק המשתמש
- Discord - אפליקציית הצ'אט
📌 סיכום
React היא הספרייה המובילה כיום לפיתוח צד לקוח (Frontend). היא מציעה גישה דקלרטיבית לבניית ממשקים, ביצועים גבוהים בזכות ה-Virtual DOM וקהילה עצומה שמלווה כל שלב בדרך.
היא מתאימה במיוחד עבור:
- אפליקציות עמוד יחיד (SPA - Single Page Application)
- ממשקים אינטראקטיביים שמגיבים בזמן אמת
- פרויקטים גדולים שמפותחים בעבודת צוות
הבא: התקנה והגדרות →
🔧 התקנה והגדרות
דרישות מקדימות
1. Node.js
הורידו והתקינו את Node.js מהאתר הרשמי: nodejs.org. Node.js כולל גם את מנהל החבילות npm, שבו נשתמש להתקנת ספריות לאורך הקורס.
לאחר ההתקנה, ודאו שהכול תקין בעזרת הפקודות הבאות:
node --version # לפחות v22
npm --version # לפחות v11
2. עורך קוד
העורך המומלץ הוא Visual Studio Code. כדאי להתקין בו את התוספים הבאים כדי לייעל את העבודה:
- ES7+ React/Redux/React-Native snippets - קיצורי דרך לכתיבת קומפוננטות ו-Hooks
- Prettier - עיצוב אוטומטי ואחיד של הקוד
- Auto Rename Tag - שינוי שם תג הפתיחה והסגירה בו-זמנית
🚀 יצירת פרויקט React
שיטה מומלצת: Vite
# יצירת פרויקט חדש
npm create vite@latest my-react-app -- --template react
# כניסה לתיקייה
cd my-react-app
# התקנת חבילות
npm install
# הרצת שרת פיתוח
npm run dev
כעת פתחו את הדפדפן בכתובת http://localhost:5173 - האפליקציה שלכם רצה.
שיטה חלופית: Create React App
npx create-react-app my-app
cd my-app
npm start
⚠️ הערה: Vite מהיר משמעותית וזו הבחירה המומלצת לכל פרויקט חדש. Create React App נחשב כיום לכלי מיושן.
📁 מבנה הפרויקט (Vite)
my-react-app/
├── node_modules/ # חבילות npm
├── public/ # קבצים סטטיים
│ └── vite.svg
├── src/ # קוד המקור
│ ├── assets/ # תמונות ומשאבים
│ ├── App.css # סגנונות הקומפוננטה הראשית
│ ├── App.jsx # קומפוננטה ראשית
│ ├── index.css # סגנונות גלובליים
│ └── main.jsx # נקודת הכניסה
├── .gitignore
├── index.html # דף ה-HTML הראשי
├── package.json # הגדרות הפרויקט
└── vite.config.js # הגדרות Vite
📄 קבצים חשובים
index.html
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>האפליקציה שלי</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
App.jsx
import './App.css'
function App() {
return (
<div>
<h1>שלום עולם! 👋</h1>
<p>זו אפליקציית React הראשונה שלי</p>
</div>
)
}
export default App
🛠️ פקודות נפוצות
| פקודה | תיאור |
|---|---|
npm run dev |
הרצת שרת פיתוח |
npm run build |
בניית גרסת production |
npm run preview |
צפייה בגרסת production |
npm install <package> |
התקנת חבילה |
📦 חבילות נפוצות
לצד React עצמה, רוב הפרויקטים נשענים על מספר חבילות משלימות. הנה הנפוצות ביותר, לפי תחום השימוש:
# ניווט (Routing)
npm install react-router-dom
# ניהול state גלובלי
npm install zustand
# או
npm install @reduxjs/toolkit react-redux
# סטיילינג
npm install styled-components
# או
npm install @emotion/react @emotion/styled
# אייקונים
npm install react-icons
# HTTP requests
npm install axios
⚙️ הגדרות מומלצות
vite.config.js עם alias
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
}
}
})
.eslintrc.cjs (לבדיקת קוד)
module.exports = {
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: 'detect' } },
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
},
}
📝 תחביר JSX
מהו JSX?
JSX (JavaScript XML) הוא תחביר שמאפשר לכתוב קוד שנראה כמו HTML ישירות בתוך JavaScript. חשוב להבין שזה אינו HTML אמיתי - לפני שהקוד מגיע לדפדפן הוא מתקמפל ל-JavaScript רגיל. JSX הוא בסך הכול "סוכר תחבירי" שהופך את בניית הממשק לקריאה ונוחה יותר.
// JSX
const element = <h1>שלום עולם!</h1>;
// מה שזה הופך להיות (React.createElement)
const element = React.createElement('h1', null, 'שלום עולם!');
📌 כללי תחביר בסיסיים
1. אלמנט שורש יחיד
כל קומפוננטה חייבת להחזיר אלמנט עוטף אחד בלבד. אפשר לעטוף את התוכן ב-div, ואם לא רוצים להוסיף תגית מיותרת ל-DOM - להשתמש ב-Fragment:
// ❌ שגוי - שני אלמנטים שורש
function Bad() {
return (
<h1>כותרת</h1>
<p>תוכן</p>
);
}
// ✅ נכון - עטיפה ב-div
function Good() {
return (
<div>
<h1>כותרת</h1>
<p>תוכן</p>
</div>
);
}
// ✅ נכון - שימוש ב-Fragment
function Better() {
return (
<>
<h1>כותרת</h1>
<p>תוכן</p>
</>
);
}
2. סגירת תגיות
כל התגיות חייבות להיסגר:
// ✅ תגיות עם תוכן
<div>תוכן</div>
<p>פסקה</p>
// ✅ תגיות self-closing
<img src="image.jpg" />
<br />
<input type="text" />
<hr />
3. camelCase לתכונות
מכיוון ש-JSX הוא בעצם JavaScript, חלק משמות התכונות משתנים לעומת HTML רגיל ונכתבים ב-camelCase. שימו לב במיוחד למילים השמורות class ו-for, שהופכות ל-className ו-htmlFor:
// HTML רגיל // JSX
// class="..." → className="..."
// for="..." → htmlFor="..."
// onclick="..." → onClick={...}
// tabindex="..." → tabIndex={...}
<label htmlFor="name" className="label">
שם:
<input id="name" tabIndex={1} />
</label>
🔤 ביטויים בסוגריים מסולסלים
הסוגריים המסולסלים {} הם הגשר בין ה-JSX ל-JavaScript: כל ביטוי שנכתב בתוכם מחושב, והתוצאה מוצגת במסך. אפשר להכניס אליהם משתנים, חישובים, קריאות לפונקציות ותנאים:
משתנים
const name = "דני";
const age = 25;
return (
<div>
<h1>שלום {name}!</h1>
<p>גיל: {age}</p>
</div>
);
ביטויים חשבוניים
return (
<div>
<p>2 + 2 = {2 + 2}</p>
<p>מספר אקראי: {Math.random()}</p>
<p>גיל בעוד 10 שנים: {age + 10}</p>
</div>
);
קריאות לפונקציות
function formatDate(date) {
return date.toLocaleDateString('he-IL');
}
return <p>תאריך: {formatDate(new Date())}</p>;
ביטויי תנאי (Ternary)
const isLoggedIn = true;
return (
<div>
{isLoggedIn ? <p>שלום משתמש!</p> : <p>אנא התחבר</p>}
</div>
);
אובייקטים ומערכים
const user = { name: "דני", age: 25 };
const colors = ["אדום", "ירוק", "כחול"];
return (
<div>
<p>שם: {user.name}</p>
<p>צבע ראשון: {colors[0]}</p>
</div>
);
🎨 סגנונות (Styles)
Inline Styles
// סגנונות כאובייקט JavaScript
const style = {
color: 'blue',
fontSize: '20px', // camelCase!
backgroundColor: '#f0f0f0',
padding: '10px'
};
return <p style={style}>טקסט מעוצב</p>;
// או inline
return (
<p style={{ color: 'red', fontWeight: 'bold' }}>
טקסט אדום ומודגש
</p>
);
CSS Classes
// App.css
// .title { color: blue; font-size: 24px; }
// .highlight { background-color: yellow; }
import './App.css';
function App() {
return (
<h1 className="title highlight">כותרת</h1>
);
}
Classes דינמיים
const isActive = true;
const isError = false;
return (
<div className={`box ${isActive ? 'active' : ''} ${isError ? 'error' : ''}`}>
תוכן
</div>
);
🧩 Fragments
Fragment מאפשר לקבץ כמה אלמנטים יחד מבלי להוסיף תגית עוטפת מיותרת ל-DOM. הוא פותר בדיוק את מגבלת "אלמנט השורש היחיד", בלי לזהם את מבנה העמוד ב-div נוסף:
import { Fragment } from 'react';
// תחביר מלא
function List() {
return (
<Fragment>
<li>פריט 1</li>
<li>פריט 2</li>
<li>פריט 3</li>
</Fragment>
);
}
// תחביר מקוצר (נפוץ יותר)
function List() {
return (
<>
<li>פריט 1</li>
<li>פריט 2</li>
<li>פריט 3</li>
</>
);
}
// עם key (רק בתחביר המלא)
function Items({ items }) {
return items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</Fragment>
));
}
💬 הערות ב-JSX
function App() {
return (
<div>
{/* זו הערה ב-JSX */}
<h1>כותרת</h1>
{/*
הערה
בכמה
שורות
*/}
<p>תוכן</p>
</div>
);
}
⚠️ דברים שאי אפשר לעשות ב-JSX
בתוך הסוגריים המסולסלים אפשר לכתוב ביטויים (expressions) בלבד - כלומר קוד שמחזיר ערך. משפטי בקרה כמו if או לולאת for אינם ביטויים, ולכן במקומם משתמשים ב-ternary, באופרטור && ובמתודה map:
// ❌ שגוי - if/else רגיל לא עובד בתוך JSX
function Bad() {
return (
<div>
{if (condition) { <p>כן</p> }} // שגיאה!
</div>
);
}
// ✅ השתמשו ב-ternary או &&
function Good() {
return (
<div>
{condition ? <p>כן</p> : <p>לא</p>}
{condition && <p>מוצג רק אם true</p>}
</div>
);
}
// ❌ for loop ישירות - לא עובד
function Bad() {
return (
<ul>
{for (let i = 0; i < 3; i++) { <li>{i}</li> }} // שגיאה!
</ul>
);
}
// ✅ השתמשו ב-map
function Good() {
return (
<ul>
{[0, 1, 2].map(i => <li key={i}>{i}</li>)}
</ul>
);
}
📋 סיכום תחביר JSX
| כלל | דוגמה |
|---|---|
| אלמנט שורש יחיד | <> ... </> |
| סגירת תגיות | <img /> |
| className | <div className="box"> |
| htmlFor | <label htmlFor="id"> |
| ביטויים בסוגריים | {variable} |
| Inline styles | style={{ color: 'red' }} |
| הערות | {/* הערה */} |
הקודם: ← התקנה והגדרות
הבא: קומפוננטות →
🧩 קומפוננטות (Components)
מהי קומפוננטה?
קומפוננטה היא יחידת בניין עצמאית וניתנת לשימוש חוזר בממשק המשתמש. אפשר לחשוב עליה כמו על קוביית לגו: כל קומפוננטה אחראית על חלק מוגדר בממשק, ומחיבור של קומפוננטות פשוטות אנחנו בונים ממשקים שלמים ומורכבים. בפועל, קומפוננטה היא פשוט פונקציית JavaScript שמחזירה JSX.
// קומפוננטה = פונקציה שמחזירה JSX
function Button() {
return <button>לחץ עליי</button>;
}
// שימוש בקומפוננטה
function App() {
return (
<div>
<Button />
<Button />
<Button />
</div>
);
}
📌 סוגי קומפוננטות
Function Components (מומלץ!)
// קומפוננטה פונקציונלית - הסטנדרט היום
function Welcome() {
return <h1>ברוכים הבאים!</h1>;
}
// או כ-Arrow Function
const Welcome = () => {
return <h1>ברוכים הבאים!</h1>;
};
// או בקיצור (implicit return)
const Welcome = () => <h1>ברוכים הבאים!</h1>;
Class Components (ישן - לידע כללי)
// קומפוננטת מחלקה - סגנון ישן
import { Component } from 'react';
class Welcome extends Component {
render() {
return <h1>ברוכים הבאים!</h1>;
}
}
💡 טיפ: העדיפו תמיד Function Components. הם קצרים וקריאים יותר, וזו הדרך היחידה לעבוד עם Hooks. קומפוננטות מחלקה (Class Components) מובאות כאן להיכרות בלבד, למקרה שתיתקלו בהן בקוד ישן.
📁 מבנה קובץ קומפוננטה
קומפוננטה בסיסית
// src/components/Header.jsx
function Header() {
return (
<header>
<h1>האתר שלי</h1>
<nav>
<a href="/">בית</a>
<a href="/about">אודות</a>
</nav>
</header>
);
}
export default Header;
קומפוננטה עם סגנונות
// src/components/Card.jsx
import './Card.css';
function Card() {
return (
<div className="card">
<h2 className="card-title">כותרת</h2>
<p className="card-content">תוכן הכרטיס</p>
</div>
);
}
export default Card;
📤 ייצוא וייבוא
Default Export
// Button.jsx - ייצוא
function Button() {
return <button>לחץ</button>;
}
export default Button;
// App.jsx - ייבוא
import Button from './components/Button';
// אפשר לקרוא לזה בכל שם
import MyButton from './components/Button';
Named Export
// components.jsx - ייצוא
export function Button() {
return <button>לחץ</button>;
}
export function Input() {
return <input type="text" />;
}
// App.jsx - ייבוא
import { Button, Input } from './components';
// חייב להשתמש בשם המדויק
שילוב
// Button.jsx
export function SmallButton() {
return <button className="small">קטן</button>;
}
function Button() {
return <button>לחץ</button>;
}
export default Button;
// App.jsx
import Button, { SmallButton } from './components/Button';
🏗️ מבנה תיקיות מומלץ
src/
├── components/
│ ├── common/ # קומפוננטות משותפות
│ │ ├── Button.jsx
│ │ ├── Button.css
│ │ ├── Input.jsx
│ │ └── Modal.jsx
│ ├── layout/ # קומפוננטות מבנה
│ │ ├── Header.jsx
│ │ ├── Footer.jsx
│ │ └── Sidebar.jsx
│ └── features/ # קומפוננטות לפי פיצ'ר
│ ├── UserProfile.jsx
│ └── ProductCard.jsx
├── pages/ # דפים
│ ├── Home.jsx
│ └── About.jsx
├── App.jsx
└── main.jsx
🔗 קינון קומפוננטות
הכוח האמיתי של React מתגלה כשמקננים קומפוננטות זו בתוך זו. קומפוננטות קטנות ופשוטות משמשות אבני בניין לקומפוננטות גדולות יותר, עד לאפליקציה שלמה:
// קומפוננטות קטנות
function Logo() {
return <img src="/logo.png" alt="לוגו" />;
}
function NavLinks() {
return (
<nav>
<a href="/">בית</a>
<a href="/about">אודות</a>
<a href="/contact">צור קשר</a>
</nav>
);
}
// קומפוננטה מורכבת
function Header() {
return (
<header className="header">
<Logo />
<NavLinks />
</header>
);
}
// אפליקציה שלמה
function App() {
return (
<div>
<Header />
<main>
<h1>ברוכים הבאים</h1>
</main>
<Footer />
</div>
);
}
✅ כללים לשמות קומפוננטות
-
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 שהיא מקבלת. כך הופכים קומפוננטה סטטית לקומפוננטה גמישה וניתנת לשימוש חוזר.
// בלי Props - קומפוננטה סטטית
function Greeting() {
return <h1>שלום דני!</h1>;
}
// עם Props - קומפוננטה דינמית
function Greeting(props) {
return <h1>שלום {props.name}!</h1>;
}
// שימוש
<Greeting name="דני" />
<Greeting name="רונית" />
<Greeting name="משה" />
📌 העברת Props
טיפוסים בסיסיים
function UserCard(props) {
return (
<div className="card">
<h2>{props.name}</h2>
<p>גיל: {props.age}</p>
<p>עיר: {props.city}</p>
</div>
);
}
// שימוש
<UserCard name="דני" age={25} city="תל אביב" />
<UserCard name="רונית" age={30} city="ירושלים" />
Destructuring (מומלץ!)
// במקום props.name, props.age...
function UserCard({ name, age, city }) {
return (
<div className="card">
<h2>{name}</h2>
<p>גיל: {age}</p>
<p>עיר: {city}</p>
</div>
);
}
🔢 סוגי Props
מחרוזות (Strings)
// מחרוזות בגרשיים
<Button text="לחץ עליי" />
<Title message="ברוכים הבאים" />
מספרים (Numbers)
// מספרים בסוגריים מסולסלים
<Counter initial={0} />
<Product price={99.99} quantity={3} />
בוליאנים (Booleans)
// true - מספיק לכתוב את השם
<Button disabled />
<Modal isOpen />
// false - צריך לציין במפורש
<Button disabled={false} />
<Modal isOpen={false} />
מערכים ואובייקטים
const colors = ['אדום', 'ירוק', 'כחול'];
const user = { name: 'דני', age: 25 };
<ColorList colors={colors} />
<Profile user={user} />
פונקציות
function handleClick() {
alert('לחצת!');
}
<Button onClick={handleClick} />
👶 Children Props
children הוא prop מיוחד שמכיל את כל מה שנכתב בין תגית הפתיחה לתגית הסגירה של הקומפוננטה. הוא מאפשר לבנות קומפוננטות "עוטפות" גמישות, כמו כרטיס או חלון קופץ, שלא יודעות מראש איזה תוכן ייכנס אליהן:
function Card({ children, title }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-content">
{children}
</div>
</div>
);
}
// שימוש
<Card title="כותרת">
<p>זה התוכן שבפנים!</p>
<button>כפתור</button>
</Card>
<Card title="כותרת אחרת">
<ul>
<li>פריט 1</li>
<li>פריט 2</li>
</ul>
</Card>
🎯 Default Props
לעיתים נרצה שלקומפוננטה יהיה ערך התחלתי סביר גם כשההורה לא מעביר prop מסוים. לשם כך מגדירים ערכי ברירת מחדל. הדרך המומלצת כיום היא לקבוע אותם ישירות ב-destructuring:
שיטה 1: Destructuring עם ברירת מחדל
function Button({ text = "לחץ", color = "blue", size = "medium" }) {
return (
<button className={`btn btn-${color} btn-${size}`}>
{text}
</button>
);
}
// שימוש
<Button /> // text="לחץ", color="blue", size="medium"
<Button text="שמור" /> // text="שמור", color="blue", size="medium"
<Button text="מחק" color="red" /> // text="מחק", color="red", size="medium"
שיטה 2: defaultProps (ישן יותר)
function Button({ text, color, size }) {
return (
<button className={`btn btn-${color} btn-${size}`}>
{text}
</button>
);
}
Button.defaultProps = {
text: "לחץ",
color: "blue",
size: "medium"
};
📐 Spread Operator עם Props
const buttonProps = {
text: "לחץ",
color: "blue",
onClick: handleClick,
disabled: false
};
// במקום לכתוב כל prop בנפרד
<Button text={buttonProps.text} color={buttonProps.color} ... />
// אפשר להשתמש ב-spread
<Button {...buttonProps} />
// שילוב: spread + override
<Button {...buttonProps} color="red" /> // color יהיה "red"
⚠️ Props הם Read-Only
עיקרון חשוב ב-React: אסור לשנות את ה-props בתוך הקומפוננטה שמקבלת אותם. ה-props שייכים להורה, והקומפוננטה מקבלת אותם לקריאה בלבד. שמירה על העיקרון הזה היא מה שהופך את זרימת הנתונים לצפויה וקלה למעקב:
function Counter({ count }) {
// ❌ שגיאה! אי אפשר לשנות props
count = count + 1;
return <p>ספירה: {count}</p>;
}
כשצריך ערך שמשתנה לאורך זמן בתוך הקומפוננטה עצמה, משתמשים ב-State - הנושא של השיעור הבא.
🔄 העברת Props בשרשרת
// App -> Header -> Logo
function App() {
const logoSrc = "/logo.png";
return <Header logo={logoSrc} />;
}
function Header({ logo }) {
return (
<header>
<Logo src={logo} />
<nav>...</nav>
</header>
);
}
function Logo({ src }) {
return <img src={src} alt="לוגו" />;
}
💡 טיפ: העברת אותו prop דרך רמות רבות (תופעה שמכונה Props Drilling) הופכת את הקוד למסורבל. במקרים כאלה שקלו להשתמש ב-Context API, שנלמד בהמשך.
📋 סיכום
| נושא | תחביר |
|---|---|
| העברת מחרוזת | <Component name="value" /> |
| העברת מספר | <Component count={5} /> |
| העברת boolean | <Component active /> |
| העברת אובייקט | <Component user={{ name: "..." }} /> |
| העברת פונקציה | <Component onClick={handleClick} /> |
| Children | <Component>תוכן</Component> |
| ברירת מחדל | function C({ prop = "default" }) |
| Spread | <Component {...props} /> |
הקודם: ← קומפוננטות
הבא: State →
🔄 State
מהו State?
State הוא נתון פנימי של קומפוננטה שיכול להשתנות לאורך זמן - למשל ספירה שעולה, טקסט שהמשתמש מקליד או חלון שנפתח ונסגר. זהו ההבדל המהותי מ-props: את ה-State הקומפוננטה מנהלת בעצמה. הכוח שלו טמון בכך שבכל פעם שה-State משתנה, React מרנדרת את הקומפוננטה מחדש ומעדכנת את המסך אוטומטית.
import { useState } from 'react';
function Counter() {
// הגדרת state עם ערך התחלתי 0
const [count, setCount] = useState(0);
return (
<div>
<p>ספירה: {count}</p>
<button onClick={() => setCount(count + 1)}>
הוסף 1
</button>
</div>
);
}
📌 useState Hook
התחביר
const [value, setValue] = useState(initialValue);
// ↑ ↑ ↑
// ערך פונקציית ערך התחלתי
// נוכחי עדכון
דוגמאות
// מספר
const [count, setCount] = useState(0);
// מחרוזת
const [name, setName] = useState("");
// בוליאני
const [isOpen, setIsOpen] = useState(false);
// מערך
const [items, setItems] = useState([]);
// אובייקט
const [user, setUser] = useState({ name: "", age: 0 });
⚡ עדכון State
עדכון ישיר
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(5)}>הגדר ל-5</button>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
</div>
);
}
עדכון מבוסס ערך קודם (מומלץ!)
function Counter() {
const [count, setCount] = useState(0);
// ✅ מומלץ - שימוש בפונקציית callback
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
const double = () => setCount(prev => prev * 2);
return (
<div>
<p>{count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={double}>×2</button>
</div>
);
}
📝 State עם טפסים
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
console.log("Email:", email);
console.log("Password:", password);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="אימייל"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="סיסמה"
/>
<button type="submit">התחבר</button>
</form>
);
}
📦 State עם אובייקטים
function UserForm() {
const [user, setUser] = useState({
name: "",
email: "",
age: 0
});
// ❌ שגוי - לא לשנות ישירות
// user.name = "דני";
// ✅ נכון - יצירת אובייקט חדש
const updateName = (name) => {
setUser({ ...user, name: name });
};
// ✅ או בגרסה מקוצרת
const updateEmail = (email) => {
setUser(prev => ({ ...prev, email }));
};
return (
<div>
<input
value={user.name}
onChange={(e) => updateName(e.target.value)}
/>
<input
value={user.email}
onChange={(e) => updateEmail(e.target.value)}
/>
</div>
);
}
📚 State עם מערכים
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "לקנות חלב", done: false },
{ id: 2, text: "לשלם חשבונות", done: false }
]);
// הוספת פריט
const addTodo = (text) => {
setTodos(prev => [...prev, {
id: Date.now(),
text,
done: false
}]);
};
// מחיקת פריט
const deleteTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
// עדכון פריט
const toggleTodo = (id) => {
setTodos(prev => prev.map(todo =>
todo.id === id
? { ...todo, done: !todo.done }
: todo
));
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>❌</button>
</li>
))}
</ul>
);
}
🔀 State vs Props
| תכונה | Props | State |
|---|---|---|
| מקור | מגיע מההורה | פנימי לקומפוננטה |
| שינוי | לא ניתן לשנות | ניתן לשנות עם setter |
| מטרה | העברת נתונים | ניהול נתונים משתנים |
| רינדור מחדש | כשההורה מתרנדר | כש-State משתנה |
// Props - נתונים מבחוץ (read-only)
function Greeting({ name }) {
return <h1>שלום {name}!</h1>;
}
// State - נתונים פנימיים (mutable)
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// שילוב: State בהורה, Props לילד
function App() {
const [name, setName] = useState("דני");
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<Greeting name={name} /> {/* State הופך ל-Props */}
</div>
);
}
⚠️ כללים חשובים
1. אל תשנו State ישירות
// ❌ שגוי
count = count + 1;
user.name = "דני";
todos.push({ id: 1, text: "חדש" });
// ✅ נכון
setCount(count + 1);
setUser({ ...user, name: "דני" });
setTodos([...todos, { id: 1, text: "חדש" }]);
2. State הוא אסינכרוני
קריאה ל-setter אינה מעדכנת את הערך באופן מיידי, אלא מתזמנת רינדור מחדש. לכן מיד אחרי הקריאה המשתנה עדיין מחזיק את הערך הישן. אם צריך לחשב על בסיס הערך העדכני, משתמשים בגרסת ה-callback (prev => ...):
function Example() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // עדיין הערך הישן!
};
}
3. מספר useState בקומפוננטה אחת
function Form() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [age, setAge] = useState(0);
const [isValid, setIsValid] = useState(false);
// ...
}
📋 סיכום
| פעולה | קוד |
|---|---|
| הגדרת State | const [x, setX] = useState(initial) |
| עדכון ישיר | setX(newValue) |
| עדכון מבוסס קודם | setX(prev => prev + 1) |
| עדכון אובייקט | setX(prev => ({...prev, key: val})) |
| הוספה למערך | setX(prev => [...prev, newItem]) |
| מחיקה ממערך | setX(prev => prev.filter(...)) |
| עדכון במערך | setX(prev => prev.map(...)) |
🎯 אירועים (Events)
מבוא
אירועים הם הדרך שבה הממשק מגיב לפעולות המשתמש - לחיצה, הקלדה, מעבר עכבר ועוד. React משתמשת במערכת אירועים דומה מאוד לזו של HTML, אך עם שני הבדלים מרכזיים:
- שמות האירועים נכתבים ב-camelCase (למשל
onClickולאonclick) - מעבירים פונקציה כערך, ולא מחרוזת של קוד
// HTML רגיל
<button onclick="handleClick()">לחץ</button>
// React
<button onClick={handleClick}>לחץ</button>
📌 אירועים נפוצים
onClick - לחיצה
function Button() {
const handleClick = () => {
alert('לחצת על הכפתור!');
};
return <button onClick={handleClick}>לחץ עליי</button>;
}
// או inline
<button onClick={() => alert('לחצת!')}>לחץ</button>
onChange - שינוי ערך
function Input() {
const [text, setText] = useState("");
const handleChange = (event) => {
setText(event.target.value);
};
return (
<input
type="text"
value={text}
onChange={handleChange}
placeholder="הקלד משהו..."
/>
);
}
onSubmit - שליחת טופס
function Form() {
const [name, setName] = useState("");
const handleSubmit = (event) => {
event.preventDefault(); // מונע רענון הדף
console.log('שם:', name);
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button type="submit">שלח</button>
</form>
);
}
🔧 רשימת אירועים
אירועי עכבר
<div
onClick={() => console.log('לחיצה')}
onDoubleClick={() => console.log('לחיצה כפולה')}
onMouseEnter={() => console.log('עכבר נכנס')}
onMouseLeave={() => console.log('עכבר יצא')}
onMouseMove={(e) => console.log(e.clientX, e.clientY)}
>
עבור עם העכבר
</div>
אירועי מקלדת
<input
onKeyDown={(e) => console.log('מקש למטה:', e.key)}
onKeyUp={(e) => console.log('מקש למעלה:', e.key)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
console.log('Enter נלחץ!');
}
}}
/>
אירועי פוקוס
<input
onFocus={() => console.log('קיבל פוקוס')}
onBlur={() => console.log('איבד פוקוס')}
/>
אירועי טופס
<form onSubmit={handleSubmit} onReset={handleReset}>
<input onChange={handleChange} />
<select onChange={handleSelect} />
<textarea onChange={handleTextChange} />
</form>
📦 Event Object
כל פונקציית handler מקבלת אוטומטית אובייקט event שמתאר את האירוע שהתרחש. דרכו ניתן לגשת לאלמנט שהפעיל את האירוע, לערך שהוזן, למקש שנלחץ ולמידע נוסף:
function EventExample() {
const handleClick = (event) => {
console.log('סוג האירוע:', event.type);
console.log('אלמנט מקור:', event.target);
console.log('מיקום X:', event.clientX);
console.log('מיקום Y:', event.clientY);
console.log('מקש Ctrl לחוץ?', event.ctrlKey);
};
const handleInput = (event) => {
console.log('ערך חדש:', event.target.value);
console.log('שם השדה:', event.target.name);
};
return (
<div>
<button onClick={handleClick}>לחץ</button>
<input name="username" onChange={handleInput} />
</div>
);
}
🛑 preventDefault ו-stopPropagation
שתי מתודות של אובייקט האירוע שכדאי להכיר: preventDefault מבטלת את התנהגות ברירת המחדל של הדפדפן (כמו רענון הדף בשליחת טופס או ניווט בלחיצה על קישור), ו-stopPropagation עוצרת את בעבוע האירוע כלפי מעלה, כך שאירוע בקומפוננטה פנימית לא יפעיל בטעות handler של קומפוננטה עוטפת:
// preventDefault - מונע התנהגות ברירת מחדל
function Link() {
const handleClick = (e) => {
e.preventDefault(); // מונע ניווט
console.log('הלינק נלחץ');
};
return <a href="https://google.com" onClick={handleClick}>לינק</a>;
}
// stopPropagation - עוצר את בועת האירוע
function Nested() {
return (
<div onClick={() => console.log('חיצוני')}>
<button onClick={(e) => {
e.stopPropagation();
console.log('פנימי');
}}>
לחץ
</button>
</div>
);
// בלי stopPropagation: "פנימי" ואז "חיצוני"
// עם stopPropagation: רק "פנימי"
}
📤 העברת פרמטרים נוספים
שיטה 1: Arrow Function
function List() {
const items = ['תפוח', 'בננה', 'תפוז'];
const handleClick = (item) => {
console.log('נבחר:', item);
};
return (
<ul>
{items.map(item => (
<li key={item}>
<button onClick={() => handleClick(item)}>
{item}
</button>
</li>
))}
</ul>
);
}
שיטה 2: data attributes
function List() {
const handleClick = (event) => {
const item = event.target.dataset.item;
console.log('נבחר:', item);
};
return (
<ul>
{['תפוח', 'בננה', 'תפוז'].map(item => (
<li key={item}>
<button data-item={item} onClick={handleClick}>
{item}
</button>
</li>
))}
</ul>
);
}
🔄 אירוע + State
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>ספירה: {count}</p>
<button onClick={() => setCount(c => c + 1)}>הוסף</button>
<button onClick={() => setCount(c => c - 1)}>הפחת</button>
<button onClick={() => setCount(0)}>אפס</button>
</div>
);
}
function Toggle() {
const [isOn, setIsOn] = useState(false);
return (
<button onClick={() => setIsOn(prev => !prev)}>
{isOn ? '🔵 פועל' : '⚪ כבוי'}
</button>
);
}
📝 דוגמה מלאה: טופס
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [submitted, setSubmitted] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('נתוני הטופס:', formData);
setSubmitted(true);
};
if (submitted) {
return <p>תודה {formData.name}! ההודעה נשלחה.</p>;
}
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="שם"
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="אימייל"
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="הודעה"
/>
<button type="submit">שלח</button>
</form>
);
}
📋 סיכום אירועים
| אירוע | שימוש |
|---|---|
onClick |
לחיצה על אלמנט |
onChange |
שינוי ערך (input, select) |
onSubmit |
שליחת טופס |
onKeyDown |
לחיצת מקש |
onMouseEnter |
עכבר נכנס |
onMouseLeave |
עכבר יוצא |
onFocus |
קבלת פוקוס |
onBlur |
איבוד פוקוס |
🪝 Hooks
מהם Hooks?
Hooks הם פונקציות מיוחדות שמאפשרות "להתחבר" ליכולות הליבה של React - ניהול State, תופעות לוואי, גישה ל-DOM ועוד - ישירות מתוך Function Components. עד שנוספו בגרסה 16.8 (שנת 2019), יכולות אלו היו זמינות רק בקומפוננטות מחלקה. ה-Hooks הם שהפכו את הקומפוננטות הפונקציונליות לדרך העבודה המקובלת ב-React.
import { useState, useEffect } from 'react';
function Counter() {
// useState הוא Hook!
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
📌 חוקי Hooks
React מזהה את ה-Hooks לפי הסדר שבו הם נקראים בכל רינדור. כדי שהסדר יישאר קבוע, יש לשמור על שני כללים ברזל:
1. רק ברמה העליונה
אין לקרוא ל-Hook בתוך תנאי, לולאה או פונקציה מקוננת - רק ישירות בגוף הקומפוננטה. כך React מבטיחה שאותם Hooks נקראים באותו סדר בכל רינדור:
// ✅ נכון - ברמה העליונה
function Component() {
const [count, setCount] = useState(0);
if (count > 5) {
// ...לוגיקה
}
}
// ❌ שגוי - בתוך תנאי
function Component() {
if (someCondition) {
const [count, setCount] = useState(0); // אסור!
}
}
// ❌ שגוי - בתוך לולאה
function Component() {
for (let i = 0; i < 3; i++) {
const [x, setX] = useState(0); // אסור!
}
}
2. רק בקומפוננטות React (או Custom Hooks)
// ✅ נכון - בקומפוננטה
function MyComponent() {
const [state, setState] = useState(0);
}
// ✅ נכון - ב-Custom Hook
function useMyHook() {
const [state, setState] = useState(0);
return state;
}
// ❌ שגוי - בפונקציה רגילה
function regularFunction() {
const [state, setState] = useState(0); // אסור!
}
🔵 useState
כבר הכרנו את ה-Hook הזה בשיעור על State. הנה תזכורת קצרה:
const [value, setValue] = useState(initialValue);
// דוגמאות
const [count, setCount] = useState(0);
const [name, setName] = useState("");
const [items, setItems] = useState([]);
const [user, setUser] = useState(null);
🟢 useEffect
Hook זה מאפשר לסנכרן את הקומפוננטה עם העולם שמחוץ ל-React - פעולות שנקראות side effects (תופעות לוואי). בין השימושים הנפוצים:
- קריאות לשרת (API)
- הרשמה לאירועים והאזנה להם (Subscriptions)
- שינוי ידני של ה-DOM
- טיימרים ומרווחי זמן
מערך התלויות ([dependencies]) הוא הלב של ה-Hook: הוא קובע מתי האפקט ירוץ מחדש. פונקציית ה-cleanup שמחזירים מנקה אחריו - למשל מבטלת טיימר או מסירה מאזין.
התחביר
useEffect(() => {
// הקוד שירוץ
return () => {
// cleanup (אופציונלי)
};
}, [dependencies]); // מערך תלויות
ריצה בכל רינדור
useEffect(() => {
console.log('רץ בכל רינדור');
});
// ללא מערך תלויות!
ריצה פעם אחת (mount)
useEffect(() => {
console.log('רץ רק פעם אחת בטעינה');
fetchData();
}, []); // מערך ריק!
ריצה כשתלויות משתנות
useEffect(() => {
console.log('count השתנה:', count);
}, [count]); // רץ כש-count משתנה
Cleanup
useEffect(() => {
const interval = setInterval(() => {
console.log('טיק');
}, 1000);
// Cleanup - רץ לפני unmount או לפני הריצה הבאה
return () => {
clearInterval(interval);
};
}, []);
דוגמה: Fetch Data
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]); // רץ מחדש כשה-userId משתנה
if (loading) return <p>טוען...</p>;
if (error) return <p>שגיאה: {error}</p>;
return <h1>{user.name}</h1>;
}
🟡 useRef
ל-useRef שני שימושים עיקריים: לגשת ישירות לאלמנט DOM (למשל כדי למקד שדה קלט), ולשמור ערך שנשמר בין רינדורים אך אינו גורם לרינדור מחדש כשהוא משתנה. זהו ההבדל המרכזי מ-State.
Reference ל-DOM
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus</button>
</div>
);
}
שמירת ערך (לא גורם לרינדור)
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>{count}</p>
<button onClick={start}>התחל</button>
<button onClick={stop}>עצור</button>
</div>
);
}
🟣 useMemo
useMemo מיישם Memoization: הוא שומר את התוצאה של חישוב "כבד" ומריץ אותו מחדש רק כשאחת התלויות משתנה. כך נמנעים מחישובים מיותרים בכל רינדור.
function ExpensiveComponent({ items, filter }) {
// מחושב מחדש רק כש-items או filter משתנים
const filteredItems = useMemo(() => {
console.log('מחשב...');
return items.filter(item => item.name.includes(filter));
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
🔴 useCallback
בדומה ל-useMemo, אך עבור פונקציות: useCallback שומר את אותה פונקציה בין רינדורים במקום ליצור אותה מחדש בכל פעם. הוא שימושי במיוחד כשמעבירים callback לקומפוננטת ילד עטופה ב-React.memo, כדי למנוע רינדור מיותר שלה.
function Parent() {
const [count, setCount] = useState(0);
// הפונקציה לא נוצרת מחדש בכל רינדור
const handleClick = useCallback(() => {
console.log('נלחץ');
}, []); // תלויות ריקות = לא משתנה לעולם
return <Child onClick={handleClick} />;
}
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>לחץ</button>;
});
🟠 useContext
מאפשר לשתף נתונים בין קומפוננטות מרוחקות בעץ מבלי להעביר props דרך כל רמת ביניים (Props Drilling). זו הצצה ראשונה ל-Context API, שנרחיב עליו בשיעור נפרד.
import { createContext, useContext, useState } from 'react';
// יצירת Context
const ThemeContext = createContext();
// Provider - עוטף את האפליקציה
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
</ThemeContext.Provider>
);
}
// צריכת ה-Context בכל קומפוננטה
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<header className={theme}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
החלף ערכת נושא
</button>
</header>
);
}
🎨 Custom Hooks
מעבר ל-Hooks המובנים, אפשר לכתוב Hooks משלנו. Custom Hook הוא פונקציה ששמה מתחיל ב-use ומרכזת בתוכה לוגיקה מבוססת-Hooks, כך שאפשר לעשות בה שימוש חוזר בכמה קומפוננטות בלי לשכפל קוד:
useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// שימוש
function App() {
const [name, setName] = useLocalStorage('name', '');
// ...
}
useWindowSize
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// שימוש
function Component() {
const { width, height } = useWindowSize();
return <p>גודל חלון: {width}x{height}</p>;
}
📋 סיכום Hooks
| Hook | שימוש |
|---|---|
useState |
ניהול state |
useEffect |
side effects (API, timers) |
useRef |
reference ל-DOM או ערך קבוע |
useMemo |
memoization של ערך מחושב |
useCallback |
memoization של פונקציה |
useContext |
שיתוף נתונים גלובלי |
הקודם: ← אירועים
הבא: רינדור מותנה →
🔀 רינדור מותנה (Conditional Rendering)
מבוא
לעיתים קרובות נרצה להציג תוכן שונה בהתאם למצב - למשל הודעת טעינה בזמן שהנתונים נטענים, או תפריט אחר למשתמש מחובר. ב-React אין תחביר תנאי מיוחד: משתמשים ב-JavaScript רגיל בתוך ה-JSX. בשיעור זה נעבור על השיטות המקובלות ונראה מתי כל אחת מהן מתאימה.
📌 שיטות לרינדור מותנה
1. אופרטור Ternary ( ? : )
function Greeting({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? (
<h1>ברוך הבא בחזרה!</h1>
) : (
<h1>אנא התחבר</h1>
)}
</div>
);
}
2. אופרטור && (Logical AND)
מציג תוכן רק אם התנאי true:
function Notifications({ count }) {
return (
<div>
{count > 0 && (
<span className="badge">{count}</span>
)}
</div>
);
}
// אם count = 0, לא יוצג כלום
// אם count = 5, יוצג <span class="badge">5</span>
⚠️ זהירות למלכודת נפוצה: האופרטור
&&מחזיר את הצד השמאלי כשהוא "נפילה" (falsy). המספר0הוא falsy, אך React דווקא מציגה אותו על המסך. לכן בודקים תמיד תנאי מפורש (למשלcount > 0) ולא נשענים על הערך עצמו:// ❌ בעיה {count && <span>{count}</span>} // אם count=0, יוצג "0" // ✅ פתרון {count > 0 && <span>{count}</span>}
3. if/else רגיל
function StatusMessage({ status }) {
let message;
if (status === 'loading') {
message = <p>טוען...</p>;
} else if (status === 'error') {
message = <p className="error">שגיאה!</p>;
} else if (status === 'success') {
message = <p className="success">הצלחה!</p>;
} else {
message = <p>לא ידוע</p>;
}
return <div>{message}</div>;
}
4. switch/case
function Icon({ type }) {
switch (type) {
case 'success':
return <span>✅</span>;
case 'error':
return <span>❌</span>;
case 'warning':
return <span>⚠️</span>;
default:
return <span>ℹ️</span>;
}
}
5. אובייקט מיפוי
function StatusIcon({ status }) {
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: 'ℹ️'
};
return <span>{icons[status] || icons.info}</span>;
}
🎯 דוגמאות מעשיות
הצגת Loading
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUsers().then(data => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) {
return <div className="spinner">טוען...</div>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
טיפול בשגיאות
function DataDisplay() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData)
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (loading) return <p>⏳ טוען...</p>;
if (error) return <p>❌ שגיאה: {error}</p>;
if (!data) return <p>📭 אין נתונים</p>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
מצב ריק
function TodoList({ todos }) {
if (todos.length === 0) {
return (
<div className="empty-state">
<p>🎉 אין משימות!</p>
<p>הוסף משימה חדשה להתחיל</p>
</div>
);
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
🙈 הסתרת קומפוננטה
החזרת null
function Modal({ isOpen, children }) {
if (!isOpen) {
return null; // לא מרנדר כלום
}
return (
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>
);
}
CSS Display
function Toggle() {
const [visible, setVisible] = useState(true);
return (
<div>
<button onClick={() => setVisible(v => !v)}>
{visible ? 'הסתר' : 'הצג'}
</button>
<div style={{ display: visible ? 'block' : 'none' }}>
תוכן שאפשר להסתיר
</div>
</div>
);
}
🎨 Classes דינמיים
function Button({ isActive, isDisabled, children }) {
// שיטה 1: Template literal
const className = `btn ${isActive ? 'active' : ''} ${isDisabled ? 'disabled' : ''}`;
// שיטה 2: Array + filter + join
const classNames = [
'btn',
isActive && 'active',
isDisabled && 'disabled'
].filter(Boolean).join(' ');
return (
<button className={classNames} disabled={isDisabled}>
{children}
</button>
);
}
📋 סיכום
| שיטה | מתי להשתמש |
|---|---|
condition ? a : b |
שני מצבים אפשריים |
condition && element |
הצגה/הסתרה פשוטה |
if/else |
לוגיקה מורכבת |
switch |
מספר מצבים קבועים |
return null |
הסתרה מלאה |
📋 רשימות (Lists)
מבוא
כמעט כל ממשק מציג נתונים בצורת רשימה - מוצרים, הודעות, משתמשים ועוד. ב-React ממירים מערך נתונים לרשימת אלמנטים בעזרת המתודה map(), שיוצרת אלמנט JSX עבור כל פריט במערך:
const fruits = ['תפוח', 'בננה', 'תפוז'];
function FruitList() {
return (
<ul>
{fruits.map(fruit => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}
🔑 חשיבות ה-key
כל פריט ברשימה חייב key - מזהה ייחודי ויציב שעוזר ל-React לעקוב אחר הפריטים בין רינדורים ולזהות איזה מהם השתנה, נוסף או נמחק. בזכות ה-key, React מעדכנת רק את מה שהשתנה במקום לבנות את כל הרשימה מחדש. המזהה הטבעי ביותר הוא בדרך כלל ה-id שמגיע מהנתונים:
✅ נכון - key ייחודי
const users = [
{ id: 1, name: 'דני' },
{ id: 2, name: 'רונית' },
{ id: 3, name: 'משה' }
];
function UserList() {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
❌ בעייתי - index כ-key
// לא מומלץ! עלול לגרום לבאגים כשהרשימה משתנה
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
כללים ל-key
- חייב להיות ייחודי בין אחים
- חייב להיות יציב - לא להשתנות בין רינדורים
- לא להשתמש ב-
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) |
הקודם: ← רינדור מותנה
הבא: תרגילים →
📝 תרגילים
תרגילים לתרגול כל הנושאים
הגיע הזמן להפוך את החומר לידע מעשי. התרגילים מסודרים לפי רמת קושי - מבסיסי ועד מתקדם. מומלץ לנסות לפתור כל תרגיל באופן עצמאי לפני שמציצים ברמז, ולבדוק את הפתרון מול הדוגמאות בתיקיית examples.
🟢 רמה בסיסית
תרגיל 1: קומפוננטת ברכה
צרו קומפוננטה Greeting שמקבלת prop בשם name ומציגה "שלום, [name]!"
// התוצאה הרצויה:
<Greeting name="דני" /> // מציג: שלום, דני!
💡 רמז
השתמשו ב-function component עם props destructuring.תרגיל 2: מונה לחיצות
צרו קומפוננטה Counter עם כפתור שסופר לחיצות.
// פונקציונליות:
// - מציג את מספר הלחיצות
// - כפתור "הוסף" שמגדיל ב-1
// - כפתור "אפס" שמחזיר ל-0
💡 רמז
השתמשו ב-useState לניהול המונה.
תרגיל 3: Toggle
צרו קומפוננטה שמחליפה בין "פועל" ו"כבוי" בלחיצה על כפתור.
💡 רמז
השתמשו ב-useState עם ערך בוליאני.
🟡 רמה בינונית
תרגיל 4: רשימת משימות
צרו אפליקציית רשימת משימות (Todo List) שכוללת:
- שדה קלט להוספת משימה חדשה
- רשימה של המשימות הקיימות
- אפשרות למחוק משימה
- אפשרות לסמן משימה כ"בוצעה"
💡 רמז
const [todos, setTodos] = useState([]);
// כל todo: { id, text, completed }
תרגיל 5: טופס הרשמה
צרו טופס הרשמה שכולל:
- שדה שם (חובה, לפחות 2 תווים)
- שדה אימייל (חובה, בפורמט תקין)
- שדה סיסמה (חובה, לפחות 6 תווים)
- הודעות שגיאה מתאימות לכל שדה
- כפתור שליחה שפעיל רק כשכל השדות תקינים
💡 רמז
השתמשו ב-state לכל שדה + state לשגיאות.תרגיל 6: Timer
צרו טיימר שכולל:
- תצוגת זמן בפורמט דקות:שניות
- כפתור "התחל"
- כפתור "עצור"
- כפתור "אפס"
💡 רמז
השתמשו ב-useEffect עם setInterval, ושמרו את ה-interval ב-useRef.
🔴 רמה מתקדמת
תרגיל 7: Fetch Data
צרו קומפוננטה שמביאה נתונים מ-API. הדרישות:
- שליפת נתונים מהכתובת
https://jsonplaceholder.typicode.com/users - הצגת מצב טעינה (loading) בזמן השליפה
- הצגת הודעת שגיאה אם השליפה נכשלה
- הצגת רשימת המשתמשים בסיום
💡 רמז
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => setUsers(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
תרגיל 8: Theme Context
צרו מערכת ערכות נושא (Theme) המבוססת על Context:
- שתי ערכות נושא: בהירה וכהה
- כפתור להחלפה ביניהן
- כל הקומפוננטות מתעדכנות בהתאם לערכה הפעילה
💡 רמז
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// ...
}
תרגיל 9: חנות (Mini E-Commerce)
צרו חנות מיני שכוללת:
- רשימת מוצרים (שם, מחיר ותמונה)
- אפשרות להוסיף מוצר לעגלה
- תצוגת עגלה עם סכום כולל לתשלום
- אפשרות להסיר מוצר מהעגלה
תרגיל 10: Custom Hook - useLocalStorage
צרו Custom Hook שעובד כמו useState אבל שומר את הנתונים ב-localStorage:
const [name, setName] = useLocalStorage('user-name', '');
// הערך נשמר גם אחרי רענון הדף!
📌 טיפים לפתרון
- התחילו פשוט - קודם גרמו לזה לעבוד, אחר כך שפרו
- חלקו לקומפוננטות - כל חלק נפרד בקומפוננטה משלו
- בדקו תוך כדי - הריצו ובדקו אחרי כל שינוי קטן
- קראו את השגיאות - הן מספרות בדיוק מה הבעיה
✅ פתרונות
הפתרונות נמצאים בתיקיית examples:
- תרגילים 1-3:
01_hello_component.jsx - תרגיל 4:
08_todo_app.jsx - תרגיל 5:
04_events_example.jsx - תרגיל 6:
05_hooks_example.jsx
בהצלחה! 🎉
הקודם: ← רשימות
הבא: Lifecycle & useEffect →
🔄 React Lifecycle & useEffect
מבוא
Lifecycle (מחזור חיים) הוא שם כולל לשלבים שכל קומפוננטה עוברת מרגע הופעתה על המסך ועד הסרתה: יצירה (Mount), עדכון (Update) והריסה (Unmount). הבנת המחזור הזה חיונית כדי לדעת מתי לטעון נתונים, מתי להגיב לשינויים ומתי לשחרר משאבים.
בקומפוננטות פונקציונליות מנהלים את כל השלבים האלה באמצעות Hook יחיד - useEffect - שבו נתמקד בשיעור הזה.
📌 מהו useEffect?
useEffect הוא Hook שמאפשר לבצע side effects - פעולות שמסנכרנות את הקומפוננטה עם משהו שנמצא מחוץ לה. בין השימושים הנפוצים:
- קריאות לשרת (API)
- הרשמה לאירועים והאזנה להם (subscriptions)
- שינויים ידניים ב-DOM
- טיימרים ומרווחי זמן
התחביר הבסיסי
import { useEffect } from 'react';
useEffect(() => {
// הקוד שירוץ (effect)
return () => {
// cleanup (אופציונלי) - רץ לפני הריסה או לפני האפקט הבא
};
}, [dependencies]); // מערך תלויות
🎯 מערך התלויות (Dependencies Array)
מערך התלויות קובע מתי האפקט ירוץ:
1. ללא מערך - רץ בכל רינדור
useEffect(() => {
console.log('רץ בכל רינדור!');
});
// ⚠️ זהירות! עלול לגרום לבעיות ביצועים
2. מערך ריק [] - רץ פעם אחת (Mount)
useEffect(() => {
console.log('רץ רק בטעינה הראשונה');
// שווה ערך ל-componentDidMount
}, []);
3. עם תלויות - רץ כשהתלות משתנה
useEffect(() => {
console.log(`הערך השתנה ל: ${value}`);
// שווה ערך ל-componentDidUpdate (עבור value)
}, [value]);
4. מספר תלויות
useEffect(() => {
console.log('אחד מהערכים השתנה');
}, [value1, value2, value3]);
🔄 השוואה ל-Class Lifecycle
| Class Component | Function Component + useEffect |
|---|---|
componentDidMount |
useEffect(() => {}, []) |
componentDidUpdate |
useEffect(() => {}, [deps]) |
componentWillUnmount |
useEffect(() => { return () => {} }, []) |
דוגמה מפורטת
// Class Component (סגנון ישן)
class Example extends React.Component {
componentDidMount() {
console.log('נטען');
document.title = 'נטען';
}
componentDidUpdate(prevProps) {
if (prevProps.count !== this.props.count) {
console.log('count השתנה');
}
}
componentWillUnmount() {
console.log('נהרס');
}
}
// Function Component (סגנון מודרני)
function Example({ count }) {
// componentDidMount
useEffect(() => {
console.log('נטען');
document.title = 'נטען';
}, []);
// componentDidUpdate for count
useEffect(() => {
console.log('count השתנה');
}, [count]);
// componentWillUnmount
useEffect(() => {
return () => {
console.log('נהרס');
};
}, []);
}
🧹 Cleanup Function
פונקציית ה-cleanup היא פונקציה שמחזירים מתוך האפקט, ותפקידה לנקות אחריו. היא רצה בשני מצבים:
- לפני הריסת הקומפוננטה (Unmount)
- לפני כל הרצה חוזרת של האפקט, כדי לבטל את ההרצה הקודמת
למה צריך Cleanup?
// ❌ בעיה: דליפת זיכרון (Memory Leak)
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// הטיימר ממשיך לרוץ גם אחרי שהקומפוננטה נהרסת!
}, []);
return <p>{seconds}</p>;
}
// ✅ פתרון: Cleanup
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup - עוצר את הטיימר
return () => {
clearInterval(interval);
};
}, []);
return <p>{seconds}</p>;
}
דוגמאות נוספות ל-Cleanup
// Event Listeners
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// WebSocket
useEffect(() => {
const socket = new WebSocket('wss://example.com');
socket.onmessage = (event) => console.log(event.data);
return () => {
socket.close();
};
}, []);
// AbortController (לביטול fetch)
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') {
setError(err.message);
}
});
return () => {
controller.abort();
};
}, []);
⚠️ Memory Leaks - דליפות זיכרון
דליפת זיכרון (Memory Leak) מתרחשת כשהקוד ממשיך לרוץ או להחזיק במשאבים גם אחרי שהקומפוננטה כבר נהרסה. הסיבות השכיחות:
- טיימרים שלא נעצרים
- מאזיני אירועים (Event listeners) שלא מוסרים
- קריאות API שלא מבוטלות
- הרשמות (Subscriptions) שלא מנותקות
סימנים לדליפת זיכרון
Warning: Can't perform a React state update on an unmounted component.
מניעת דליפות
function DataFetcher({ url }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true; // דגל לבדיקה
async function fetchData() {
try {
const res = await fetch(url);
const json = await res.json();
// עדכון רק אם הקומפוננטה עדיין קיימת
if (isMounted) {
setData(json);
setLoading(false);
}
} catch (error) {
if (isMounted) {
setLoading(false);
}
}
}
fetchData();
return () => {
isMounted = false; // סימון שהקומפוננטה נהרסה
};
}, [url]);
if (loading) return <p>טוען...</p>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
📝 דפוסים נפוצים
1. Fetch בטעינה
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(setUsers);
}, []);
2. עדכון כותרת הדף
useEffect(() => {
document.title = `${count} הודעות חדשות`;
}, [count]);
3. הרשמה לאירועי חלון
useEffect(() => {
const handleScroll = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
4. Debounce בחיפוש
useEffect(() => {
const timeoutId = setTimeout(() => {
if (searchTerm) {
searchAPI(searchTerm).then(setResults);
}
}, 300);
return () => clearTimeout(timeoutId);
}, [searchTerm]);
5. סנכרון עם localStorage
useEffect(() => {
localStorage.setItem('theme', theme);
}, [theme]);
❌ טעויות נפוצות
1. שכחת תלויות
// ❌ שגוי - count לא במערך התלויות
useEffect(() => {
console.log(count); // תמיד יהיה הערך ההתחלתי
}, []);
// ✅ נכון
useEffect(() => {
console.log(count);
}, [count]);
2. לולאה אינסופית
// ❌ שגוי - גורם ללולאה אינסופית
useEffect(() => {
setData([...data, newItem]); // data משתנה → אפקט רץ שוב → ...
}, [data]);
// ✅ נכון - שימוש ב-callback
useEffect(() => {
setData(prev => [...prev, newItem]);
}, [newItem]);
3. אובייקט/מערך כתלות
// ❌ בעייתי - אובייקט חדש בכל רינדור
const options = { page: 1, limit: 10 };
useEffect(() => {
fetch('/api', options);
}, [options]); // רץ בכל רינדור!
// ✅ פתרון 1: useMemo
const options = useMemo(() => ({ page, limit }), [page, limit]);
// ✅ פתרון 2: תלויות פרימיטיביות
useEffect(() => {
fetch('/api', { page, limit });
}, [page, limit]);
📋 סיכום
| מצב | תחביר |
|---|---|
| Mount (פעם אחת) | useEffect(() => {}, []) |
| Update (כשמשתנה) | useEffect(() => {}, [dep]) |
| Unmount (cleanup) | useEffect(() => () => {}, []) |
| כל רינדור | useEffect(() => {}) |
הקודם: ← תרגילים
הבא: Props Drilling & Lifting State →
🔗 Props Drilling & Lifting State Up
מבוא
ככל שאפליקציה גדלה ועץ הקומפוננטות מסתעף, צצות שתי בעיות נפוצות בניהול הנתונים:
- Props Drilling - הצורך להעביר props דרך רמות ביניים רבות שאינן זקוקות להם
- שיתוף State - מצב שבו כמה קומפוננטות צריכות לקרוא ולעדכן את אותו נתון
בשיעור זה נכיר את שתי הבעיות ואת הפתרון הבסיסי לשיתוף State - Lifting State Up.
📌 Props Drilling - הבעיה
Props Drilling קורה כשמעבירים props דרך קומפוננטות ביניים שלא צריכות אותם:
// ❌ Props Drilling - הנתון עובר דרך 3 רמות!
function App() {
const [user, setUser] = useState({ name: 'דני' });
return <Layout user={user} />; // רמה 1
}
function Layout({ user }) {
return (
<div>
<Sidebar user={user} /> {/* רמה 2 - לא צריך את user */}
<Content />
</div>
);
}
function Sidebar({ user }) {
return (
<div>
<UserProfile user={user} /> {/* רמה 3 - לא צריך את user */}
</div>
);
}
function UserProfile({ user }) {
return <p>שלום, {user.name}!</p>; // רמה 4 - כאן באמת צריך!
}
בעיות של Props Drilling:
- קוד מסורבל וקשה לתחזוקה
- קומפוננטות ביניים "מזוהמות" ב-props לא רלוונטיים
- קשה לשנות את מבנה הנתונים
🔝 Lifting State Up - פתרון לשיתוף State
העיקרון של Lifting State Up ("הרמת המצב") פשוט: כאשר כמה קומפוננטות צריכות לשתף נתון, מעבירים אותו למעלה - אל קומפוננטת ההורה המשותפת הקרובה ביותר. ההורה מחזיק את ה-State ומעביר אותו לילדים כ-props, יחד עם פונקציית עדכון. כך כל הילדים עובדים על אותו מקור נתונים יחיד ונשארים מסונכרנים.
הבעיה: שתי קומפוננטות צריכות את אותו נתון
// ❌ בעיה - כל קומפוננטה עם state נפרד
function TemperatureInput() {
const [celsius, setCelsius] = useState(0);
return <input value={celsius} onChange={e => setCelsius(e.target.value)} />;
}
function TemperatureDisplay() {
const [celsius, setCelsius] = useState(0); // לא מסונכרן!
return <p>{celsius}°C = {(celsius * 9/5) + 32}°F</p>;
}
הפתרון: Lifting State Up
// ✅ פתרון - State בהורה המשותף
function TemperatureConverter() {
// State מורם להורה
const [celsius, setCelsius] = useState(0);
return (
<div>
<TemperatureInput
value={celsius}
onChange={setCelsius}
/>
<TemperatureDisplay celsius={celsius} />
</div>
);
}
function TemperatureInput({ value, onChange }) {
return (
<input
type="number"
value={value}
onChange={e => onChange(Number(e.target.value))}
/>
);
}
function TemperatureDisplay({ celsius }) {
const fahrenheit = (celsius * 9/5) + 32;
return <p>{celsius}°C = {fahrenheit}°F</p>;
}
📝 דוגמה מלאה: טופס עם ולידציה
function RegistrationForm() {
// State מורם - כל הנתונים בהורה
const [formData, setFormData] = useState({
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
// פונקציית עדכון שמועברת לילדים
const updateField = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
// ולידציה
const validate = () => {
const newErrors = {};
if (!formData.email.includes('@')) {
newErrors.email = 'אימייל לא תקין';
}
if (formData.password.length < 6) {
newErrors.password = 'סיסמה חייבת להכיל לפחות 6 תווים';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'הסיסמאות לא תואמות';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
return (
<form onSubmit={e => { e.preventDefault(); validate(); }}>
<EmailInput
value={formData.email}
onChange={v => updateField('email', v)}
error={errors.email}
/>
<PasswordInput
value={formData.password}
onChange={v => updateField('password', v)}
error={errors.password}
/>
<ConfirmPasswordInput
value={formData.confirmPassword}
onChange={v => updateField('confirmPassword', v)}
error={errors.confirmPassword}
/>
<FormSummary formData={formData} errors={errors} />
<button type="submit">הרשם</button>
</form>
);
}
function EmailInput({ value, onChange, error }) {
return (
<div>
<input
type="email"
value={value}
onChange={e => onChange(e.target.value)}
placeholder="אימייל"
/>
{error && <span className="error">{error}</span>}
</div>
);
}
function PasswordInput({ value, onChange, error }) {
return (
<div>
<input
type="password"
value={value}
onChange={e => onChange(e.target.value)}
placeholder="סיסמה"
/>
{error && <span className="error">{error}</span>}
</div>
);
}
function ConfirmPasswordInput({ value, onChange, error }) {
return (
<div>
<input
type="password"
value={value}
onChange={e => onChange(e.target.value)}
placeholder="אימות סיסמה"
/>
{error && <span className="error">{error}</span>}
</div>
);
}
function FormSummary({ formData, errors }) {
const isValid = Object.keys(errors).length === 0 &&
formData.email && formData.password;
return (
<div className="summary">
<p>סטטוס: {isValid ? '✅ תקין' : '❌ לא מלא'}</p>
</div>
);
}
🔄 דפוס: Controlled Component
קומפוננטה מבוקרת (Controlled Component) היא קומפוננטה שאינה מחזיקה State משלה, אלא מקבלת את הערך שלה מההורה ומדווחת לו על כל שינוי. זהו יישום ישיר של Lifting State Up, והדפוס המקובל לעבודה עם שדות טופס ב-React:
// ✅ Controlled Component
function TextInput({ value, onChange, label }) {
return (
<label>
{label}:
<input
value={value} // ערך מההורה
onChange={e => onChange(e.target.value)} // עדכון להורה
/>
</label>
);
}
// שימוש
function Parent() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<div>
<TextInput label="שם" value={name} onChange={setName} />
<TextInput label="אימייל" value={email} onChange={setEmail} />
</div>
);
}
📊 מתי להשתמש בכל גישה?
| מצב | פתרון מומלץ |
|---|---|
| 2-3 קומפוננטות משתפות state | Lifting State Up |
| Props עוברים דרך 3+ רמות | Context API (פרק הבא) |
| State גלובלי מורכב | Redux / Zustand |
| State מקומי בלבד | useState רגיל |
⚠️ טעויות נפוצות
1. הרמה לא נכונה
// ❌ שגוי - State ברמה גבוהה מדי
function App() {
const [inputValue, setInputValue] = useState(''); // לא צריך להיות כאן!
return <DeepNestedComponent value={inputValue} />;
}
// ✅ נכון - State בהורה המשותף הקרוב ביותר
function FormSection() {
const [inputValue, setInputValue] = useState('');
return <SomeInput value={inputValue} onChange={setInputValue} />;
}
2. שכפול State
// ❌ שגוי - State משוכפל
function Child({ user }) {
const [localUser, setLocalUser] = useState(user); // שכפול!
// localUser לא יתעדכן כשהprop משתנה
}
// ✅ נכון - שימוש ישיר ב-prop
function Child({ user, onUserChange }) {
// ללא state מקומי, עובדים ישירות עם ה-prop
}
📋 סיכום
| מושג | הסבר |
|---|---|
| Props Drilling | העברת props דרך קומפוננטות ביניים |
| Lifting State Up | העלאת state להורה משותף |
| Controlled Component | קומפוננטה ש-state שלה מנוהל בחוץ |
| Single Source of Truth | מקור אחד לנתון |
הקודם: ← Lifecycle & useEffect
הבא: Component Communication Patterns →
🔀 Component Communication Patterns
מבוא
אפליקציה אמיתית בנויה מעשרות קומפוננטות שצריכות להעביר מידע ביניהן. הדרך הנכונה לתקשר תלויה בקשר בין הקומפוננטות בעץ. אלו דפוסי התקשורת המרכזיים, מהפשוט למתקדם:
- Parent → Child: העברת נתונים כלפי מטה באמצעות Props
- Child → Parent: החזרת מידע כלפי מעלה באמצעות פונקציות Callback
- Siblings (אחים): תקשורת דרך ההורה המשותף (Lifting State Up)
- Deep / Global: שיתוף רחב באמצעות Context API או ספריית ניהול State
📌 1. Parent → Child (Props)
הדרך הפשוטה ביותר - העברת נתונים מהורה לילד:
// Parent מעביר נתונים ל-Child
function Parent() {
const data = { name: 'דני', age: 25 };
return <Child user={data} />;
}
function Child({ user }) {
return <p>שלום {user.name}, בן {user.age}</p>;
}
📌 2. Child → Parent (Callbacks)
הילד קורא לפונקציה שנשלחה מההורה:
// Parent מקבל נתונים מ-Child
function Parent() {
const [message, setMessage] = useState('');
const handleMessage = (msg) => {
console.log('התקבל מהילד:', msg);
setMessage(msg);
};
return (
<div>
<Child onSendMessage={handleMessage} />
<p>הודעה מהילד: {message}</p>
</div>
);
}
function Child({ onSendMessage }) {
return (
<button onClick={() => onSendMessage('שלום מהילד!')}>
שלח הודעה להורה
</button>
);
}
דוגמה מלאה: טופס
function ProductForm() {
const handleSubmit = (productData) => {
console.log('מוצר חדש:', productData);
// שליחה לשרת וכו'
};
return <ProductFormInputs onSubmit={handleSubmit} />;
}
function ProductFormInputs({ onSubmit }) {
const [name, setName] = useState('');
const [price, setPrice] = useState('');
const handleFormSubmit = (e) => {
e.preventDefault();
onSubmit({ name, price: Number(price) });
};
return (
<form onSubmit={handleFormSubmit}>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="שם מוצר"
/>
<input
type="number"
value={price}
onChange={e => setPrice(e.target.value)}
placeholder="מחיר"
/>
<button type="submit">הוסף מוצר</button>
</form>
);
}
📌 3. Sibling Communication
קומפוננטות אחיות מתקשרות דרך ההורה המשותף:
function App() {
const [selectedItem, setSelectedItem] = useState(null);
return (
<div className="app">
{/* אח 1: בוחר פריט */}
<ItemList
onSelect={setSelectedItem}
/>
{/* אח 2: מציג את הבחירה */}
<ItemDetails
item={selectedItem}
/>
</div>
);
}
function ItemList({ onSelect }) {
const items = ['פריט א', 'פריט ב', 'פריט ג'];
return (
<ul>
{items.map(item => (
<li key={item} onClick={() => onSelect(item)}>
{item}
</li>
))}
</ul>
);
}
function ItemDetails({ item }) {
if (!item) return <p>בחר פריט מהרשימה</p>;
return <p>נבחר: {item}</p>;
}
📌 4. Event Emitter Pattern
דפוס מתקדם המבוסס על "אפיק אירועים" מרכזי (Event Bus): קומפוננטה אחת משדרת אירוע והאחרות מאזינות לו, בלי קשר ישיר ביניהן. ברוב המקרים Context API יהיה פשוט ונקי יותר, אך כדאי להכיר את הדפוס:
// פשוט יותר להשתמש ב-Context, אבל זה דפוס אפשרי
const eventBus = {
events: {},
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
// Unsubscribe function
return () => {
this.events[event] = this.events[event].filter(cb => cb !== callback);
};
},
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
};
function ComponentA() {
return (
<button onClick={() => eventBus.emit('message', 'שלום!')}>
שלח הודעה
</button>
);
}
function ComponentB() {
const [message, setMessage] = useState('');
useEffect(() => {
const unsubscribe = eventBus.subscribe('message', setMessage);
return unsubscribe;
}, []);
return <p>הודעה: {message}</p>;
}
📌 5. Compound Components Pattern
דפוס שבו קבוצת קומפוננטות עובדת יחד כיחידה אחת וחולקת State פנימי, בעוד המשתמש מרכיב אותן בחופשיות. דוגמה קלאסית היא רכיב טאבים (Tabs ו-Tab), שמעניק API הצהרתי ונקי:
// קומפוננטות שמשתפות state באופן מובנה
function Tabs({ children, defaultTab = 0 }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<div className="tabs">
<div className="tab-list">
{React.Children.map(children, (child, index) => (
<button
className={activeTab === index ? 'active' : ''}
onClick={() => setActiveTab(index)}
>
{child.props.label}
</button>
))}
</div>
<div className="tab-content">
{React.Children.toArray(children)[activeTab]}
</div>
</div>
);
}
function Tab({ children, label }) {
return <div>{children}</div>;
}
// שימוש
function App() {
return (
<Tabs defaultTab={0}>
<Tab label="פרופיל">
<h2>פרופיל משתמש</h2>
<p>תוכן הפרופיל...</p>
</Tab>
<Tab label="הגדרות">
<h2>הגדרות</h2>
<p>תוכן ההגדרות...</p>
</Tab>
<Tab label="התראות">
<h2>התראות</h2>
<p>תוכן ההתראות...</p>
</Tab>
</Tabs>
);
}
📌 6. Render Props Pattern
דפוס שבו קומפוננטה מקבלת פונקציית רינדור כ-prop, ומעבירה אליה את ה-State הפנימי שלה. כך אפשר לשתף לוגיקה (כמו מעקב אחר מיקום העכבר) בין קומפוננטות, בעוד כל אחת מהן מחליטה בעצמה כיצד להציג את התוצאה:
// קומפוננטה שחושפת state דרך render prop
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// קוראים לפונקציה שהתקבלה עם ה-state
return render(position);
}
// שימוש
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<div>
<p>מיקום העכבר: {x}, {y}</p>
<div
style={{
position: 'absolute',
left: x,
top: y,
width: 20,
height: 20,
background: 'red',
borderRadius: '50%'
}}
/>
</div>
)}
/>
);
}
📊 השוואה בין דפוסים
| דפוס | שימוש | יתרונות | חסרונות |
|---|---|---|---|
| Props | Parent → Child | פשוט, ברור | Props drilling |
| Callbacks | Child → Parent | גמיש | יכול להסתבך |
| Lifting State | Siblings | מקור אחד לאמת | קוד בהורה |
| Context | Deep/Global | ללא drilling | Rerenders |
| Compound | קומפוננטות קשורות | API נקי | מורכב |
| Render Props | לוגיקה משותפת | גמיש מאוד | Callback hell |
📋 סיכום
// Parent → Child
<Child data={data} />
// Child → Parent
<Child onChange={(value) => setParentState(value)} />
// Siblings (דרך הורה)
<Brother1 onSelect={setShared} />
<Brother2 selected={shared} />
הקודם: ← Props Drilling
הבא: Context API →
🌐 Context API
מבוא
Context API הוא המנגנון המובנה של React לשיתוף נתונים "גלובליים" בתוך עץ הקומפוננטות, והפתרון הישיר לבעיית ה-Props Drilling. במקום להעביר prop יד ביד דרך כל רמת ביניים, מגדירים Context פעם אחת - וכל קומפוננטה בתוך העץ יכולה לגשת אליו ישירות. הוא מתאים במיוחד לנתונים שנצרכים במקומות רבים, כמו ערכת נושא, שפה או פרטי המשתמש המחובר.
ללא Context: עם Context:
App ──► Layout App (Provider)
──► Sidebar ├── Layout
──► UserCard ├── Sidebar
──► Avatar └── Avatar (Consumer)
↑ נגיש ישירות!
📌 שלושת השלבים
העבודה עם Context בנויה תמיד משלושה שלבים: יוצרים את ה-Context, עוטפים את העץ ב-Provider שמספק את הערך, וצורכים את הערך בכל קומפוננטה שצריכה אותו.
1. Create - יצירת Context
import { createContext } from 'react';
// יצירת Context עם ערך ברירת מחדל
const ThemeContext = createContext('light');
// או בלי ערך ברירת מחדל
const UserContext = createContext(null);
2. Provide - הספקת הערך
import { ThemeContext } from './ThemeContext';
function App() {
const [theme, setTheme] = useState('dark');
return (
// Provider עוטף את כל הקומפוננטות שצריכות גישה
<ThemeContext.Provider value={theme}>
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
);
}
3. Consume - צריכת הערך
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedButton() {
// שימוש ב-useContext לקבלת הערך
const theme = useContext(ThemeContext);
return (
<button className={`btn-${theme}`}>
כפתור ב-{theme} mode
</button>
);
}
📝 דוגמה מלאה: Theme Context
קובץ ThemeContext.jsx
import { createContext, useContext, useState } from 'react';
// יצירת Context
const ThemeContext = createContext(null);
// Provider Component
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
// ערך ה-Context כולל את ה-state והפונקציות
const value = {
theme,
toggleTheme,
isDark: theme === 'dark'
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// Custom Hook לשימוש נוח
export function useTheme() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
שימוש
// App.jsx
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
);
}
// Header.jsx
import { useTheme } from './ThemeContext';
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
</header>
);
}
// ThemedCard.jsx
import { useTheme } from './ThemeContext';
function ThemedCard({ children }) {
const { isDark } = useTheme();
return (
<div style={{
backgroundColor: isDark ? '#333' : '#fff',
color: isDark ? '#fff' : '#333',
padding: '20px'
}}>
{children}
</div>
);
}
📝 דוגמה: Auth Context
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// בדיקת משתמש מחובר בטעינה
const savedUser = localStorage.getItem('user');
if (savedUser) {
setUser(JSON.parse(savedUser));
}
setLoading(false);
}, []);
const login = async (email, password) => {
// קריאה ל-API
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const userData = await response.json();
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
};
const logout = () => {
setUser(null);
localStorage.removeItem('user');
};
const value = {
user,
isAuthenticated: !!user,
loading,
login,
logout
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// שימוש בקומפוננטות
function LoginButton() {
const { isAuthenticated, logout } = useAuth();
if (isAuthenticated) {
return <button onClick={logout}>התנתק</button>;
}
return <a href="/login">התחבר</a>;
}
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) return <p>טוען...</p>;
if (!isAuthenticated) return <Navigate to="/login" />;
return children;
}
🔄 שילוב מספר Contexts
function App() {
return (
<AuthProvider>
<ThemeProvider>
<LanguageProvider>
<Router>
<MainApp />
</Router>
</LanguageProvider>
</ThemeProvider>
</AuthProvider>
);
}
// קומפוננטה שמשתמשת במספר contexts
function Dashboard() {
const { user } = useAuth();
const { theme } = useTheme();
const { language } = useLanguage();
return (
<div className={theme}>
<h1>{language === 'he' ? 'שלום' : 'Hello'}, {user.name}</h1>
</div>
);
}
⚠️ Context Performance
חשוב להכיר את מחיר הביצועים: בכל פעם שערך ה-Context משתנה, כל הקומפוננטות שצורכות אותו מתרנדרות מחדש. לכן כדאי לפצל Context לפי תחומים ולייצב את הערך שמעבירים, כפי שנראה בהמשך:
בעיה
// ❌ כל שינוי ב-count גורם לרינדור של כל הצרכנים
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [count, setCount] = useState(0);
return (
<AppContext.Provider value={{ user, setUser, count, setCount }}>
{children}
</AppContext.Provider>
);
}
פתרון: הפרדת Contexts
// ✅ הפרדה לפי תחום
const UserContext = createContext(null);
const CounterContext = createContext(null);
function Providers({ children }) {
return (
<UserProvider>
<CounterProvider>
{children}
</CounterProvider>
</UserProvider>
);
}
פתרון: useMemo
function AppProvider({ children }) {
const [user, setUser] = useState(null);
// מניעת יצירת אובייקט חדש בכל רינדור
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
📊 Consumer (סגנון ישן)
לפני שהגיע ה-Hook בשם useContext, צרכו את ה-Context בעזרת רכיב Consumer ופונקציית render. התחביר עדיין נתמך, אך useContext קריא ונוח בהרבה ומומלץ לכל קוד חדש:
// סגנון ישן עם Consumer
<ThemeContext.Consumer>
{theme => (
<div className={theme}>תוכן</div>
)}
</ThemeContext.Consumer>
// סגנון מודרני עם useContext
function Component() {
const theme = useContext(ThemeContext);
return <div className={theme}>תוכן</div>;
}
📋 סיכום
| פעולה | קוד |
|---|---|
| יצירה | const Ctx = createContext(default) |
| הספקה | <Ctx.Provider value={val}> |
| צריכה | const val = useContext(Ctx) |
| Custom Hook | useMyContext() עם בדיקת שגיאה |
הקודם: ← Component Patterns
הבא: Custom Hooks →
🪝 Custom Hooks
מבוא
Custom Hook הוא פונקציה ששמה מתחיל ב-use ומשתמשת בתוכה ב-Hooks אחרים. תפקידו לחלץ לוגיקה החוזרת על עצמה - כמו שליפת נתונים מהשרת או האזנה לגודל החלון - למקום אחד, כדי שאפשר יהיה לעשות בה שימוש חוזר במספר קומפוננטות בלי לשכפל קוד. זהו אחד הכלים העוצמתיים ביותר לשמירה על קוד נקי ומאורגן ב-React.
// במקום לשכפל קוד...
function Component1() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => { /* fetch logic */ }, []);
}
function Component2() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => { /* same fetch logic */ }, []);
}
// ...יוצרים Custom Hook!
function useFetch(url) {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => { /* fetch logic */ }, [url]);
return { loading, data };
}
function Component1() {
const { loading, data } = useFetch('/api/data1');
}
📌 כללים ל-Custom Hooks
- שם מתחיל ב-
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)
מבוא
דפוסי עיצוב (Design Patterns) הם פתרונות מוכחים לבעיות שחוזרות שוב ושוב בפיתוח. ב-React התגבשו לאורך השנים כמה דפוסים מרכזיים לשיתוף לוגיקה ולטיפול במצבים מיוחדים. בשיעור זה נכיר את HOC, Render Props, Error Boundaries ו-Fragments, ונבין מתי כדאי להשתמש בכל אחד מהם.
📌 HOC - Higher Order Component
HOC (רכיב מסדר גבוה) הוא פונקציה שמקבלת קומפוננטה ומחזירה קומפוננטה חדשה, "עטופה", עם יכולות נוספות. זו דרך להוסיף התנהגות משותפת (כמו בדיקת הרשאות או מצב טעינה) למספר קומפוננטות, בלי לשכתב כל אחת מהן. כיום Custom Hooks מחליפים את רוב השימושים ב-HOC, אך הדפוס עדיין נפוץ בספריות רבות:
התחביר
// HOC = פונקציה שמקבלת קומפוננטה
const withEnhancement = (WrappedComponent) => {
// ומחזירה קומפוננטה חדשה
return function EnhancedComponent(props) {
// עם לוגיקה נוספת
return <WrappedComponent {...props} extraProp="value" />;
};
};
דוגמה: withLoading
// HOC שמוסיף מצב טעינה
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return (
<div className="loading">
<span className="spinner">⏳</span>
<p>טוען...</p>
</div>
);
}
return <WrappedComponent {...props} />;
};
}
// קומפוננטה רגילה
function UserList({ users }) {
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
// קומפוננטה משופרת
const UserListWithLoading = withLoading(UserList);
// שימוש
function App() {
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState([]);
return <UserListWithLoading isLoading={loading} users={users} />;
}
דוגמה: withAuth
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return <p>בודק הרשאות...</p>;
}
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <WrappedComponent {...props} />;
};
}
// שימוש
const ProtectedDashboard = withAuth(Dashboard);
const ProtectedSettings = withAuth(Settings);
דוגמה: withLogger
function withLogger(WrappedComponent) {
return function LoggedComponent(props) {
useEffect(() => {
console.log(`${WrappedComponent.name} mounted`);
return () => console.log(`${WrappedComponent.name} unmounted`);
}, []);
useEffect(() => {
console.log(`${WrappedComponent.name} props:`, props);
});
return <WrappedComponent {...props} />;
};
}
📌 Render Props
Render Props הוא דפוס שבו קומפוננטה מקבלת פונקציה כ-prop (לרוב ה-children) וקוראת לה עם הנתונים הפנימיים שלה. כך הקומפוננטה מנהלת את הלוגיקה, ומי שמשתמש בה שולט בתצוגה - שיתוף לוגיקה תוך שמירה על גמישות מלאה בעיצוב:
התחביר
// קומפוננטה עם render prop
function DataProvider({ render }) {
const [data, setData] = useState(null);
// לוגיקה לקבלת data...
return render(data);
}
// שימוש
<DataProvider
render={(data) => <DisplayComponent data={data} />}
/>
דוגמה: Mouse Position
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// קוראים ל-children כפונקציה עם הנתונים
return children(position);
}
// שימוש
function App() {
return (
<MouseTracker>
{({ x, y }) => (
<div>
<p>מיקום: {x}, {y}</p>
<div
style={{
position: 'absolute',
left: x - 10,
top: y - 10,
width: 20,
height: 20,
background: 'red',
borderRadius: '50%'
}}
/>
</div>
)}
</MouseTracker>
);
}
דוגמה: Toggle
function Toggle({ children }) {
const [isOn, setIsOn] = useState(false);
const toggle = () => setIsOn(prev => !prev);
return children({ isOn, toggle });
}
// שימוש
function App() {
return (
<Toggle>
{({ isOn, toggle }) => (
<div>
<button onClick={toggle}>
{isOn ? 'כבה' : 'הפעל'}
</button>
{isOn && <p>התוכן מוצג!</p>}
</div>
)}
</Toggle>
);
}
📌 Error Boundaries
Error Boundary הוא קומפוננטה שתופסת שגיאות JavaScript שמתרחשות בקומפוננטות שמתחתיה בעץ, ומציגה במקומן ממשק חלופי במקום שכל האפליקציה תקרוס. אפשר לחשוב עליו כמו try/catch ברמת ה-UI.
⚠️ Error Boundary הוא היוצא מן הכלל היחיד שבו עדיין נדרשת קומפוננטת מחלקה (Class Component), משום שהוא מסתמך על מתודות מחזור חיים שאין להן חלופה ב-Hooks.
התחביר
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
// נקרא כשיש שגיאה בילד
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// לוגים ודיווח
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error);
console.error('Error info:', errorInfo);
// שליחה לשירות דיווח שגיאות
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-fallback">
<h2>😰 משהו השתבש</h2>
<p>אנא רענן את הדף</p>
<button onClick={() => window.location.reload()}>
רענן
</button>
</div>
);
}
return this.props.children;
}
}
שימוש
function App() {
return (
<ErrorBoundary fallback={<p>שגיאה באפליקציה</p>}>
<Header />
<ErrorBoundary fallback={<p>שגיאה בתוכן</p>}>
<MainContent />
</ErrorBoundary>
<Footer />
</ErrorBoundary>
);
}
// קומפוננטה שעלולה לזרוק שגיאה
function BuggyComponent() {
const [shouldError, setShouldError] = useState(false);
if (shouldError) {
throw new Error('שגיאה מכוונת!');
}
return (
<button onClick={() => setShouldError(true)}>
גרום לשגיאה
</button>
);
}
Error Boundary עם Reset
class ErrorBoundaryWithReset extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
handleReset = () => {
this.setState({ hasError: false });
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>אופס! משהו השתבש</h2>
<button onClick={this.handleReset}>נסה שוב</button>
</div>
);
}
return this.props.children;
}
}
📌 Fragments
כבר פגשנו את Fragment בשיעור על JSX. כתזכורת: הוא מאפשר לקבץ כמה אלמנטים תחת אלמנט שורש יחיד, בלי להוסיף תגית עוטפת מיותרת ל-DOM - מה שחשוב במיוחד בהקשרים כמו טבלאות ורשימות, שבהם div נוסף עלול לשבור את המבנה:
תחביר
// תחביר מלא
import { Fragment } from 'react';
function List() {
return (
<Fragment>
<li>פריט 1</li>
<li>פריט 2</li>
<li>פריט 3</li>
</Fragment>
);
}
// תחביר מקוצר (נפוץ יותר)
function List() {
return (
<>
<li>פריט 1</li>
<li>פריט 2</li>
<li>פריט 3</li>
</>
);
}
Fragment עם Key
// כשצריך key, חייבים תחביר מלא
function Glossary({ items }) {
return (
<dl>
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</Fragment>
))}
</dl>
);
}
למה להשתמש ב-Fragments?
// ❌ בעייתי - div מיותר שמשפיע על CSS
function Columns() {
return (
<div> {/* div מיותר! */}
<td>עמודה 1</td>
<td>עמודה 2</td>
</div>
);
}
// ✅ נכון - Fragment לא יוצר אלמנט
function Columns() {
return (
<>
<td>עמודה 1</td>
<td>עמודה 2</td>
</>
);
}
📊 השוואה: HOC vs Render Props vs Custom Hooks
| דפוס | יתרונות | חסרונות | מתי להשתמש |
|---|---|---|---|
| HOC | שימוש חוזר, הפרדה | Wrapper hell, props conflict | Cross-cutting concerns |
| Render Props | גמישות, שקיפות | Callback hell, פחות קריא | UI דינמי מאוד |
| Custom Hooks | פשוט, קריא, מודרני | רק לוגיקה (לא UI) | מועדף ברוב המקרים |
המרה מ-HOC ל-Custom Hook
// HOC (ישן)
function withWindowSize(WrappedComponent) {
return function(props) {
const [size, setSize] = useState(getSize());
useEffect(() => { /* listener */ }, []);
return <WrappedComponent {...props} windowSize={size} />;
};
}
// Custom Hook (מודרני ומועדף)
function useWindowSize() {
const [size, setSize] = useState(getSize());
useEffect(() => { /* listener */ }, []);
return size;
}
📋 סיכום
| Pattern | תחביר |
|---|---|
| HOC | withX(Component) |
| Render Props | <Provider>{(data) => ...}</Provider> |
| Error Boundary | Class with getDerivedStateFromError |
| Fragment | <>...</> או <Fragment> |
הקודם: ← Custom Hooks
הבא: React Router →
🧭 React Router
מבוא
React עצמה מטפלת ברכיב בודד, ולא בניווט בין דפים. כאן נכנסת React Router - ספריית הניתוב (Routing) הנפוצה ביותר לאפליקציות React. היא מאפשרת לבנות אפליקציית עמוד יחיד (SPA) שמתנהגת כאילו יש בה כמה דפים: הכתובת בשורת הדפדפן משתנה, אך במקום לטעון מחדש את כל העמוד, React Router פשוט מחליפה את הקומפוננטה המוצגת. התוצאה היא מעבר מהיר וחלק בין מסכים.
התקנה
npm install react-router-dom
📌 הגדרה בסיסית
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
{/* תפריט ניווט */}
<nav>
<Link to="/">בית</Link>
<Link to="/about">אודות</Link>
<Link to="/contact">צור קשר</Link>
</nav>
{/* הגדרת נתיבים */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
function Home() {
return <h1>דף הבית</h1>;
}
function About() {
return <h1>אודות</h1>;
}
function Contact() {
return <h1>צור קשר</h1>;
}
function NotFound() {
return <h1>404 - הדף לא נמצא</h1>;
}
🔗 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 (נתיבים דינמיים)
לעיתים קרובות נרצה נתיב שמשתנה לפי נתון - למשל עמוד פרופיל נפרד לכל משתמש. במקום להגדיר נתיב לכל אחד, מגדירים נתיב אחד עם פרמטר דינמי (המסומן בנקודתיים, למשל :userId), וקוראים את ערכו בעזרת ה-Hook בשם useParams:
הגדרת פרמטרים
function App() {
return (
<Routes>
<Route path="/users" element={<UserList />} />
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/products/:category/:productId"element={<Product />} />
</Routes>
);
}
קריאת פרמטרים עם useParams
import { useParams } from 'react-router-dom';
function UserProfile() {
const { userId } = useParams();
return <h1>פרופיל משתמש: {userId}</h1>;
}
function Product() {
const { category, productId } = useParams();
return (
<div>
<h1>קטגוריה: {category}</h1>
<h2>מוצר: {productId}</h2>
</div>
);
}
דוגמה מלאה
function UserList() {
const users = [
{ id: 1, name: 'דני' },
{ id: 2, name: 'רונית' },
{ id: 3, name: 'משה' }
];
return (
<div>
<h1>רשימת משתמשים</h1>
<ul>
{users.map(user => (
<li key={user.id}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
</div>
);
}
function UserProfile() {
const { userId } = useParams();
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
if (!user) return <p>טוען...</p>;
return (
<div>
<h1>{user.name}</h1>
<p>אימייל: {user.email}</p>
</div>
);
}
📁 Nested Routes (נתיבים מקוננים)
נתיבים מקוננים מאפשרים לשתף מבנה (Layout) בין כמה מסכים - למשל תפריט וכותרת קבועים, שבתוכם מתחלף רק התוכן. מגדירים נתיבי ילד בתוך נתיב הורה, וקומפוננטת ההורה מציינת בעזרת <Outlet /> היכן יוצג הנתיב הפעיל:
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
</Route>
</Routes>
);
}
Outlet - הצגת נתיב ילד
import { Outlet, Link } from 'react-router-dom';
function Layout() {
return (
<div>
<header>
<nav>
<Link to="/">בית</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
</header>
<main>
<Outlet /> {/* כאן יוצג הנתיב הילד */}
</main>
<footer>פוטר</footer>
</div>
);
}
function Dashboard() {
return (
<div className="dashboard">
<aside>
<nav>
<Link to="/dashboard">סקירה</Link>
<Link to="/dashboard/profile">פרופיל</Link>
<Link to="/dashboard/settings">הגדרות</Link>
</nav>
</aside>
<div className="content">
<Outlet /> {/* תתי-נתיבים */}
</div>
</div>
);
}
🔍 Query Parameters
פרמטרים של שאילתה (החלק שאחרי ה-? בכתובת, למשל ?q=react&page=2) מתאימים לנתונים אופציונליים כמו מונחי חיפוש, סינון ומספר עמוד. ה-Hook בשם useSearchParams מאפשר לקרוא ולעדכן אותם בדומה ל-useState:
import { useSearchParams } from 'react-router-dom';
function SearchResults() {
const [searchParams, setSearchParams] = useSearchParams();
// קריאה
const query = searchParams.get('q');
const page = searchParams.get('page') || '1';
const sort = searchParams.get('sort') || 'date';
// עדכון
const updateSearch = (newQuery) => {
setSearchParams({ q: newQuery, page: '1' });
};
const nextPage = () => {
setSearchParams({
q: query,
page: String(Number(page) + 1),
sort
});
};
return (
<div>
<input
value={query || ''}
onChange={(e) => updateSearch(e.target.value)}
placeholder="חפש..."
/>
<p>מציג תוצאות עבור: "{query}"</p>
<p>עמוד: {page}</p>
<button onClick={nextPage}>עמוד הבא</button>
</div>
);
}
🚀 Programmatic Navigation
לצד ניווט בלחיצה על קישור, לעיתים צריך לנווט מתוך הקוד עצמו - למשל להעביר את המשתמש ללוח הבקרה מיד לאחר התחברות מוצלחת. לשם כך משתמשים ב-Hook בשם useNavigate:
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
const success = await login(email, password);
if (success) {
// ניווט לאחר התחברות
navigate('/dashboard');
// או עם replace (לא נשמר בהיסטוריה)
navigate('/dashboard', { replace: true });
// או אחורה
navigate(-1);
}
};
return <form onSubmit={handleSubmit}>...</form>;
}
🔒 Protected Routes
נתיבים מוגנים חוסמים גישה למסכים מסוימים ממשתמשים שאינם מחוברים. מממשים זאת בעזרת קומפוננטת עטיפה שבודקת את מצב ההתחברות, ומפנה ל-login אם צריך - תוך שמירת היעד המקורי כדי לחזור אליו לאחר ההתחברות:
import { Navigate, useLocation } from 'react-router-dom';
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
const location = useLocation();
if (loading) {
return <p>טוען...</p>;
}
if (!isAuthenticated) {
// שמירת המיקום המבוקש לאחר התחברות
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// שימוש
function App() {
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
</Routes>
);
}
// ניווט חזרה אחרי התחברות
function Login() {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || '/';
const handleLogin = async () => {
await login();
navigate(from, { replace: true });
};
}
📍 useLocation Hook
import { useLocation } from 'react-router-dom';
function CurrentPath() {
const location = useLocation();
// location.pathname = '/users/123'
// location.search = '?tab=profile'
// location.hash = '#section1'
// location.state = { from: ... }
return (
<div>
<p>נתיב: {location.pathname}</p>
<p>Query: {location.search}</p>
</div>
);
}
📋 סיכום
| Component/Hook | שימוש |
|---|---|
BrowserRouter |
עוטף את האפליקציה |
Routes |
מכיל את הנתיבים |
Route |
הגדרת נתיב בודד |
Link |
קישור לניווט |
NavLink |
קישור עם active state |
Outlet |
הצגת nested route |
useParams |
קריאת פרמטרים דינמיים |
useSearchParams |
query string |
useNavigate |
ניווט בקוד |
useLocation |
מידע על הנתיב הנוכחי |
הקודם: ← React Patterns
הבא: Performance Optimization →
⚡ Performance Optimization
מבוא
React מהירה כברירת מחדל, אך באפליקציות גדולות רינדורים מיותרים עלולים להצטבר ולפגוע בחוויית המשתמש. בשיעור זה נכיר את שלושת הכלים המרכזיים לאופטימיזציה - React.memo, useMemo ו-useCallback. חשוב לזכור כלל אצבע: אין לבצע אופטימיזציה בטרם עת. משתמשים בכלים האלה כשמזהים בעיית ביצועים אמיתית, לא באופן גורף.
📌 React.memo
React.memo עוטף קומפוננטה ו"מזכיר" את התוצאה האחרונה שלה: אם ה-props לא השתנו מהרינדור הקודם, React מדלגת על הרינדור ומשתמשת בתוצאה השמורה. זה יעיל במיוחד לקומפוננטות "כבדות" שמקבלות את אותם props אך מתרנדרות שוב ושוב בגלל שינוי בהורה:
הבעיה
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
{count}
</button>
{/* ExpensiveList מתרנדר בכל שינוי של count! */}
<ExpensiveList items={items} />
</div>
);
}
function ExpensiveList({ items }) {
console.log('ExpensiveList rendered!'); // נקרא בכל רינדור
return <ul>{items.map(item => <li key={item}>{item}</li>)}</ul>;
}
הפתרון
// עטיפה ב-React.memo
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
console.log('ExpensiveList rendered!'); // נקרא רק כש-items משתנה
return <ul>{items.map(item => <li key={item}>{item}</li>)}</ul>;
});
Custom Comparison
const MemoizedComponent = React.memo(
function MyComponent({ user, onClick }) {
return <div onClick={onClick}>{user.name}</div>;
},
// Custom comparison function
(prevProps, nextProps) => {
// return true = לא לרנדר מחדש
// return false = לרנדר מחדש
return prevProps.user.id === nextProps.user.id;
}
);
📌 useMemo
אם React.memo מזכיר קומפוננטה שלמה, useMemo מזכיר תוצאה של חישוב בודד בתוך קומפוננטה. הוא שומר את הערך המחושב ומריץ את החישוב מחדש רק כשאחת התלויות משתנה - שימושי לחישובים כבדים או לייצוב אובייקטים ומערכים שמועברים כ-props:
מתי להשתמש?
- חישובים כבדים
- יצירת אובייקטים/מערכים גדולים
- מניעת רינדורים מיותרים ב-children
התחביר
const memoizedValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]); // מחושב מחדש רק כש-a או b משתנים
דוגמאות
function ProductList({ products, filter, sort }) {
// ❌ מחושב מחדש בכל רינדור
const filteredProducts = products
.filter(p => p.category === filter)
.sort((a, b) => sort === 'price' ? a.price - b.price : a.name.localeCompare(b.name));
// ✅ מחושב מחדש רק כשצריך
const filteredProducts = useMemo(() => {
console.log('Filtering and sorting...');
return products
.filter(p => p.category === filter)
.sort((a, b) => sort === 'price' ? a.price - b.price : a.name.localeCompare(b.name));
}, [products, filter, sort]);
return (
<ul>
{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
מניעת רינדור ילדים
function Parent() {
const [count, setCount] = useState(0);
// ❌ אובייקט חדש בכל רינדור
const style = { color: 'red', fontSize: 20 };
// ✅ אותו reference
const style = useMemo(() => ({ color: 'red', fontSize: 20 }), []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoizedChild style={style} />
</div>
);
}
📌 useCallback
useCallback הוא המקבילה של useMemo עבור פונקציות: הוא שומר את אותה פונקציה בין רינדורים ויוצר חדשה רק כשהתלויות משתנות. זה קריטי כשמעבירים פונקציה לקומפוננטת ילד עטופה ב-React.memo, שכן פונקציה חדשה בכל רינדור הייתה מבטלת את היתרון שלה:
למה זה חשוב?
function Parent() {
const [count, setCount] = useState(0);
// ❌ פונקציה חדשה בכל רינדור
const handleClick = () => {
console.log('clicked');
};
// ✅ אותה פונקציה (אותו reference)
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <MemoizedButton onClick={handleClick} />;
}
const MemoizedButton = React.memo(function Button({ onClick }) {
console.log('Button rendered');
return <button onClick={onClick}>לחץ</button>;
});
עם תלויות
function SearchComponent({ query }) {
const [results, setResults] = useState([]);
// פונקציה שתלויה ב-query
const search = useCallback(async () => {
const data = await fetchResults(query);
setResults(data);
}, [query]); // נוצרת מחדש רק כש-query משתנה
useEffect(() => {
search();
}, [search]);
}
🔄 השוואה: useMemo vs useCallback
// useMemo - שומר ערך
const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);
// useCallback - שומר פונקציה
const memoizedFn = useCallback(() => doSomething(a, b), [a, b]);
// useCallback זה בעצם:
const memoizedFn = useMemo(() => () => doSomething(a, b), [a, b]);
📊 מתי להשתמש?
| כלי | מתי להשתמש |
|---|---|
React.memo |
קומפוננטה שמתרנדרת הרבה עם אותם props |
useMemo |
חישוב כבד / אובייקט שמועבר כ-prop |
useCallback |
פונקציה שמועברת ל-memoized child |
כלל אצבע
// ❌ לא צריך - פשוט מדי
const doubled = useMemo(() => count * 2, [count]);
// ✅ כן צריך - חישוב כבד
const filteredList = useMemo(() =>
hugeList.filter(item => complexFilter(item, criteria)),
[hugeList, criteria]
);
⚠️ טעויות נפוצות
1. Memoization מיותרת
// ❌ לא יעזור - הקומפוננטה ההורה לא memo
const MemoizedChild = React.memo(Child);
function Parent() {
return <MemoizedChild data={data} />;
}
2. Dependencies חסרות
// ❌ שגוי - count לא ב-dependencies
const handleClick = useCallback(() => {
console.log(count); // תמיד הערך הראשוני!
}, []);
// ✅ נכון
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
3. אובייקט inline
// ❌ אובייקט חדש בכל רינדור - memo לא עוזר
<MemoizedChild options={{ sort: true }} />
// ✅ useMemo
const options = useMemo(() => ({ sort: true }), []);
<MemoizedChild options={options} />
🛠️ כלי Debug
React DevTools Profiler
// הוספת displayName לזיהוי קל
const MemoizedComponent = React.memo(function MyComponent(props) {
return <div>{props.value}</div>;
});
MemoizedComponent.displayName = 'MemoizedComponent';
Why Did You Render
npm install @welldone-software/why-did-you-render
// wdyr.js
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, { trackAllPureComponents: true });
}
// Component.jsx
MyComponent.whyDidYouRender = true;
📋 סיכום
| Hook/API | תחביר | שימוש |
|---|---|---|
React.memo |
React.memo(Component) |
מניעת רינדור |
useMemo |
useMemo(() => value, [deps]) |
שמירת ערך |
useCallback |
useCallback(fn, [deps]) |
שמירת פונקציה |
הקודם: ← React Router
הבא: Lazy Loading & Suspense →
📦 Lazy Loading, Suspense & Code Splitting
מבוא
כברירת מחדל, כל קוד האפליקציה נארז לקובץ אחד גדול שהמשתמש מוריד בטעינה הראשונה. ככל שהאפליקציה גדלה, הטעינה מואטת. שלוש הטכניקות בשיעור זה פותרות זאת יחד:
- Code Splitting - חלוקת הקוד למספר חבילות קטנות (chunks) במקום קובץ ענק אחד
- Lazy Loading - טעינת קומפוננטה רק ברגע שהיא באמת נדרשת
- Suspense - הצגת ממשק טעינה זמני בזמן שהקומפוננטה העצלה נטענת
📌 React.lazy
React.lazy עוטף ייבוא דינמי (import()) והופך קומפוננטה לכזו שנטענת רק בעת הצורך. הקוד שלה יופרד אוטומטית לחבילה נפרדת. מכיוון שהטעינה לוקחת רגע, חובה לעטוף את הקומפוננטה ב-Suspense עם ממשק טעינה חלופי:
התחביר
import React, { lazy, Suspense } from 'react';
// במקום import רגיל
// import HeavyComponent from './HeavyComponent';
// טעינה עצלה
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>טוען...</div>}>
<HeavyComponent />
</Suspense>
);
}
📌 Suspense
Suspense מציג תוכן חלופי (fallback) בזמן טעינה.
import { Suspense, lazy } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<div>
<h1>האפליקציה שלי</h1>
<Suspense fallback={<LoadingSpinner />}>
<LazyComponent />
</Suspense>
</div>
);
}
function LoadingSpinner() {
return (
<div className="spinner-container">
<div className="spinner"></div>
<p>טוען...</p>
</div>
);
}
Suspense מקונן
function App() {
return (
<Suspense fallback={<FullPageLoader />}>
<Header />
<Suspense fallback={<ContentLoader />}>
<MainContent />
</Suspense>
<Suspense fallback={<SidebarLoader />}>
<Sidebar />
</Suspense>
</Suspense>
);
}
📌 Code Splitting עם Routes
הדרך הנפוצה ביותר - חלוקה לפי דפים:
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// טעינה עצלה של כל דף
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
function PageLoader() {
return (
<div className="page-loader">
<div className="progress-bar"></div>
</div>
);
}
📌 Named Exports
React.lazy מצפה לייבוא ברירת מחדל (default export). כדי לטעון בעצלתיים רכיב שיוצא כ-named export, יש "לעטוף" אותו ולהחזיר אותו כ-default:
// ❌ לא עובד ישירות
const MyComponent = lazy(() => import('./Components').MyComponent);
// ✅ פתרון
const MyComponent = lazy(() =>
import('./Components').then(module => ({ default: module.MyComponent }))
);
📌 Error Handling
שילוב Suspense עם Error Boundary:
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
📌 Preloading
אפשר לשפר עוד את חוויית המשתמש על ידי טעינה מוקדמת של קומפוננטות שסביר שיידרשו בקרוב - למשל להתחיל לטעון את עמוד לוח הבקרה כבר כשהעכבר מרחף מעל הקישור אליו, כך שכשהמשתמש ילחץ הוא כבר יהיה מוכן:
const LazyDashboard = lazy(() => import('./Dashboard'));
function Navigation() {
// טעינה מוקדמת כשהעכבר מעל הלינק
const handleMouseEnter = () => {
import('./Dashboard'); // מתחיל לטעון ברקע
};
return (
<nav>
<Link
to="/dashboard"
onMouseEnter={handleMouseEnter}
>
Dashboard
</Link>
</nav>
);
}
Prefetch עם Webpack
// prefetch - עדיפות נמוכה (idle time)
const LazyComponent = lazy(() =>
import(/* webpackPrefetch: true */ './Component')
);
// preload - עדיפות גבוהה (מיד)
const LazyComponent = lazy(() =>
import(/* webpackPreload: true */ './Component')
);
📌 Dynamic Import עם תנאי
function FeatureComponent({ type }) {
const [Component, setComponent] = useState(null);
useEffect(() => {
async function loadComponent() {
let module;
if (type === 'chart') {
module = await import('./ChartComponent');
} else if (type === 'table') {
module = await import('./TableComponent');
} else {
module = await import('./DefaultComponent');
}
setComponent(() => module.default);
}
loadComponent();
}, [type]);
if (!Component) return <Loading />;
return <Component />;
}
📊 ניתוח Bundle
webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
visualizer({
open: true,
filename: 'bundle-stats.html'
})
]
};
📋 Best Practices
1. Route-based Splitting
// מומלץ - כל דף נטען בנפרד
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
2. Component-based Splitting
// קומפוננטות כבדות
const HeavyEditor = lazy(() => import('./HeavyEditor'));
const ChartLibrary = lazy(() => import('./ChartLibrary'));
3. Feature-based Splitting
// פיצ'רים שלא כולם משתמשים
const AdminPanel = lazy(() => import('./features/AdminPanel'));
const Analytics = lazy(() => import('./features/Analytics'));
📋 סיכום
| API | שימוש |
|---|---|
React.lazy() |
טעינה דינמית של קומפוננטה |
Suspense |
הצגת fallback בזמן טעינה |
import() |
Dynamic import של מודול |
webpackPrefetch |
טעינה ברקע (idle) |
webpackPreload |
טעינה מוקדמת (מיד) |
הקודם: ← Performance
הבא: useReducer →
🔀 useReducer Hook
מבוא
useReducer הוא Hook לניהול State מורכב. הוא מבוסס על אותו רעיון כמו Redux: במקום לעדכן את ה-State ישירות, שולחים "פעולה" (action) לפונקציה אחת מרכזית - ה-reducer - שאחראית על כל לוגיקת העדכון. הוא חלופה ל-useState, ומתאים יותר כאשר:
- ה-State מכיל כמה ערכים הקשורים זה לזה
- לוגיקת העדכון מורכבת ומתפצלת למקרים רבים
- העדכון הבא תלוי במצב הקודם
📌 התחביר הבסיסי
const [state, dispatch] = useReducer(reducer, initialState);
הרכיבים:
- state - המצב הנוכחי
- dispatch - פונקציה לשליחת פעולות (actions)
- reducer - פונקציה שמחשבת את ה-state החדש
- initialState - ערך התחלתי
📌 Reducer Function
ה-reducer הוא לב המנגנון: פונקציה טהורה שמקבלת את ה-State הנוכחי ואת ה-action שנשלח, ומחזירה את ה-State החדש. בדרך כלל בונים אותה כ-switch שמטפל בכל סוג פעולה בנפרד, ותמיד מחזירים אובייקט State חדש במקום לשנות את הקיים:
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'RESET':
return { ...state, count: 0 };
default:
return state;
}
}
📝 דוגמה: Counter
import { useReducer } from 'react';
// הגדרת reducer
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'INCREMENT_BY':
return { count: state.count + action.payload };
case 'RESET':
return { count: 0 };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
// הערך ההתחלתי
const initialState = { count: 0 };
function Counter() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div>
<p>ספירה: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
+1
</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>
-1
</button>
<button onClick={() => dispatch({ type: 'INCREMENT_BY', payload: 5 })}>
+5
</button>
<button onClick={() => dispatch({ type: 'RESET' })}>
אפס
</button>
</div>
);
}
📝 דוגמה: Todo List
// סוגי פעולות
const ACTIONS = {
ADD: 'ADD_TODO',
TOGGLE: 'TOGGLE_TODO',
DELETE: 'DELETE_TODO',
EDIT: 'EDIT_TODO',
CLEAR_COMPLETED:'CLEAR_COMPLETED'
};
// Reducer
function todoReducer(state, action) {
switch (action.type) {
case ACTIONS.ADD:
return [
...state,
{
id: Date.now(),
text: action.payload,
completed: false
}
];
case ACTIONS.TOGGLE:
return state.map(todo =>
todo.id === action.payload ?
{ ...todo, completed: !todo.completed }
: todo
);
case ACTIONS.DELETE:
return state.filter(todo => todo.id !== action.payload);
case ACTIONS.EDIT:
return state.map(todo =>
todo.id === action.payload.id ?
{ ...todo, text: action.payload.text }
: todo
);
case ACTIONS.CLEAR_COMPLETED:
return state.filter(todo => !todo.completed);
default:
return state;
}
}
// קומפוננטה
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [text, setText] = useState('');
const handleAdd = () => {
if (text.trim()) {
dispatch({ type: ACTIONS.ADD, payload: text });
setText('');
}
};
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAdd()}
/>
<button onClick={handleAdd}>הוסף</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({
type: ACTIONS.TOGGLE,
payload: todo.id
})}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => dispatch({
type: ACTIONS.DELETE,
payload: todo.id
})}>
❌
</button>
</li>
))}
</ul>
<button onClick={() => dispatch({ type: ACTIONS.CLEAR_COMPLETED })}>
נקה הושלמו
</button>
</div>
);
}
📝 דוגמה: Form State
const initialFormState = {
name: '',
email: '',
password: '',
errors: {},
isSubmitting: false,
isSubmitted: false
};
function formReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
[action.field]: action.value,
errors: {
...state.errors,
[action.field]: null // ניקוי שגיאה בעת הקלדה
}
};
case 'SET_ERROR':
return {
...state,
errors: {
...state.errors,
[action.field]: action.error
}
};
case 'SET_ERRORS':
return {
...state,
errors: action.errors
};
case 'SUBMIT_START':
return {
...state,
isSubmitting: true,
errors: {}
};
case 'SUBMIT_SUCCESS':
return {
...state,
isSubmitting: false,
isSubmitted: true
};
case 'SUBMIT_FAILURE':
return {
...state,
isSubmitting: false,
errors: action.errors
};
case 'RESET':
return initialFormState;
default:
return state;
}
}
function RegistrationForm() {
const [state, dispatch] = useReducer(formReducer, initialFormState);
const handleChange = (field) => (e) => {
dispatch({ type: 'SET_FIELD', field, value: e.target.value });
};
const validate = () => {
const errors = {};
if (!state.name) errors.name = 'שם נדרש';
if (!state.email.includes('@')) errors.email = 'אימייל לא תקין';
if (state.password.length < 6) errors.password = 'לפחות 6 תווים';
return errors;
};
const handleSubmit = async (e) => {
e.preventDefault();
const errors = validate();
if (Object.keys(errors).length > 0) {
dispatch({ type: 'SET_ERRORS', errors });
return;
}
dispatch({ type: 'SUBMIT_START' });
try {
await submitForm(state);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (error) {
dispatch({
type: 'SUBMIT_FAILURE',
errors: { form: error.message }
});
}
};
if (state.isSubmitted) {
return <p>✅ ההרשמה הצליחה!</p>;
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
value={state.name}
onChange={handleChange('name')}
placeholder="שם"
/>
{state.errors.name && <span className="error">{state.errors.name}</span>}
</div>
<div>
<input
type="email"
value={state.email}
onChange={handleChange('email')}
placeholder="אימייל"
/>
{state.errors.email && <span className="error">{state.errors.email}</span>}
</div>
<div>
<input
type="password"
value={state.password}
onChange={handleChange('password')}
placeholder="סיסמה"
/>
{state.errors.password && <span className="error">{state.errors.password}</span>}
</div>
<button type="submit" disabled={state.isSubmitting}>
{state.isSubmitting ? 'שולח...' : 'הרשמה'}
</button>
</form>
);
}
🔄 useState vs useReducer
| תכונה | useState | useReducer |
|---|---|---|
| מורכבות | פשוט | מורכב |
| מספר ערכים | אחד או מעט | הרבה קשורים |
| לוגיקת עדכון | פשוטה | מורכבת |
| תלות בערך קודם | לא מומלץ | מתאים |
| Testing | קשה יותר | קל יותר |
מתי להשתמש ב-useReducer?
// ✅ useState - פשוט
const [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
// ✅ useReducer - מורכב
const [formState, dispatch] = useReducer(formReducer, initialFormState);
// formState = { name, email, password, errors, isSubmitting, ... }
// ✅ useReducer - עדכונים תלויים
const [history, dispatch] = useReducer(historyReducer, { past: [], present: null, future: [] });
📌 useReducer עם Context
שילוב useReducer עם Context הוא דפוס עוצמתי ונפוץ: ה-reducer מרכז את כל לוגיקת העדכון, וה-Context מפיץ את ה-State ואת פונקציית ה-dispatch לכל האפליקציה. התוצאה היא ניהול State גלובלי מסודר, ללא ספרייה חיצונית - מעין "Redux מוקטן" מובנה:
const TodoContext = createContext();
function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<TodoContext.Provider value={{ todos, dispatch }}>
{children}
</TodoContext.Provider>
);
}
function useTodos() {
return useContext(TodoContext);
}
// שימוש
function TodoList() {
const { todos, dispatch } = useTodos();
// ...
}
📋 סיכום
| מושג | הסבר |
|---|---|
useReducer |
Hook לניהול state מורכב |
reducer |
פונקציה (state, action) => newState |
dispatch |
פונקציה לשליחת actions |
action |
אובייקט עם type ו-payload |
// תבנית בסיסית
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'ACTION_TYPE', payload: data });
הקודם: ← Lazy Loading
הבא: Redux Toolkit →
🏪 Redux Toolkit - יסודות
מבוא
Redux היא ספריית ניהול State גלובלי הנפוצה ביותר באקוסיסטם של React. היא מרכזת את כל מצב האפליקציה ב"מחסן" (store) יחיד, ומגדירה זרימת נתונים ברורה וצפויה לעדכונו. בעבר Redux נחשבה למרובת קוד ומסורבלת, ולכן צוות Redux יצר את Redux Toolkit (RTK) - הדרך הרשמית והמומלצת כיום, שמפחיתה דרמטית את כמות הקוד ומגיעה עם הגדרות מיטביות מובנות.
התקנה
npm install @reduxjs/toolkit react-redux
📌 מושגי יסוד
| מושג | הסבר |
|---|---|
| Store | מאגר ה-state המרכזי |
| Action | אובייקט שמתאר שינוי |
| Reducer | פונקציה שמחשבת state חדש |
| Dispatch | שליחת action ל-store |
| Selector | פונקציה לקריאת state |
| Slice | "פרוסה" של ה-store לתחום מסוים |
📌 יצירת Store
// store/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
import todosReducer from './todosSlice';
// יצירת ה-store
const store = configureStore({
reducer: {
counter: counterReducer,
todos: todosReducer
}
});
export default store;
📌 יצירת Slice
Slice ("פרוסה") הוא לב העבודה עם RTK: הוא מרכז במקום אחד את שם התחום, ה-State ההתחלתי וה-reducers שלו. מתוך ה-reducers ש-createSlice מקבל, הוא מייצר אוטומטית את ה-actions המתאימים. בזכות ספריית Immer המובנית, אפשר לכתוב בתוך ה-reducers קוד שנראה כמו שינוי ישיר של ה-State - ומאחורי הקלעים נוצר State חדש ותקין:
// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
// כל פונקציה היא reducer וגם יוצרת action
increment: (state) => {
// Immer מאפשר "מוטציה" כאילו!
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
reset: (state) => {
state.value = 0;
}
}
});
// ייצוא ה-actions
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
// ייצוא ה-reducer
export default counterSlice.reducer;
// Selector
export const selectCount = (state) => state.counter.value;
📌 Provider
כדי שכל הקומפוננטות יוכלו לגשת ל-store, עוטפים את שורש האפליקציה ברכיב Provider של react-redux ומעבירים לו את ה-store. זהו שלב חד-פעמי שמבוצע בנקודת הכניסה של האפליקציה:
// main.jsx / index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store/store';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
📌 שימוש בקומפוננטות
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount, reset, selectCount } from './store/counterSlice';
function Counter() {
// קריאה מה-store
const count = useSelector(selectCount);
// קבלת dispatch
const dispatch = useDispatch();
return (
<div>
<h2>ספירה: {count}</h2>
<button onClick={() => dispatch(increment())}>
+1
</button>
<button onClick={() => dispatch(decrement())}>
-1
</button>
<button onClick={() => dispatch(incrementByAmount(10))}>
+10
</button>
<button onClick={() => dispatch(reset())}>
אפס
</button>
</div>
);
}
📝 דוגמה מלאה: Todo Slice
// store/todosSlice.js
import { createSlice, nanoid } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
filter: 'all', // 'all' | 'active' | 'completed'
isLoading: false
},
reducers: {
addTodo: {
reducer: (state, action) => {
state.items.push(action.payload);
},
// prepare מאפשר לעבד את ה-payload לפני הגעה ל-reducer
prepare: (text) => ({
payload: {
id: nanoid(),
text,
completed: false,
createdAt: new Date().toISOString()
}
})
},
toggleTodo: (state, action) => {
const todo = state.items.find(t => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
editTodo: (state, action) => {
const { id, text } = action.payload;
const todo = state.items.find(t => t.id === id);
if (todo) {
todo.text = text;
}
},
deleteTodo: (state, action) => {
state.items = state.items.filter(t => t.id !== action.payload);
},
clearCompleted: (state) => {
state.items = state.items.filter(t => !t.completed);
},
setFilter: (state, action) => {
state.filter = action.payload;
}
}
});
export const {
addTodo,
toggleTodo,
editTodo,
deleteTodo,
clearCompleted,
setFilter
} = todosSlice.actions;
export default todosSlice.reducer;
// Selectors
export const selectAllTodos = (state) => state.todos.items;
export const selectFilter = (state) => state.todos.filter;
export const selectFilteredTodos = (state) => {
const { items, filter } = state.todos;
switch (filter) {
case 'active':
return items.filter(t => !t.completed);
case 'completed':
return items.filter(t => t.completed);
default:
return items;
}
};
export const selectTodosStats = (state) => {
const items = state.todos.items;
return {
total: items.length,
active: items.filter(t => !t.completed).length,
completed: items.filter(t => t.completed).length
};
};
שימוש
function TodoApp() {
const todos = useSelector(selectFilteredTodos);
const stats = useSelector(selectTodosStats);
const filter = useSelector(selectFilter);
const dispatch = useDispatch();
const [text, setText] = useState('');
const handleAdd = () => {
if (text.trim()) {
dispatch(addTodo(text));
setText('');
}
};
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAdd()}
/>
<button onClick={handleAdd}>הוסף</button>
<div>
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => dispatch(setFilter('all'))}
>
הכל ({stats.total})
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => dispatch(setFilter('active'))}
>
פעילות ({stats.active})
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => dispatch(setFilter('completed'))}
>
הושלמו ({stats.completed})
</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => dispatch(deleteTodo(todo.id))}>❌</button>
</li>
))}
</ul>
<button onClick={() => dispatch(clearCompleted())}>
נקה הושלמו
</button>
</div>
);
}
📋 סיכום
| API | שימוש |
|---|---|
configureStore |
יצירת store |
createSlice |
יצירת slice (reducer + actions) |
Provider |
עטיפת האפליקציה |
useSelector |
קריאה מה-store |
useDispatch |
קבלת dispatch |
הקודם: ← useReducer
הבא: Redux Hooks & Async →
🔌 Redux Hooks & Async Actions
מבוא
לאחר שהקמנו את ה-store וה-slices, הגיע הזמן להעמיק בעבודה השוטפת מול Redux מתוך הקומפוננטות. שיעור זה מכסה:
- שימוש מעמיק ב-
useSelectorוב-useDispatch - פעולות אסינכרוניות (כמו קריאות שרת) בעזרת
createAsyncThunk - Middleware - שכבת ביניים בזרימת ה-actions
- דפוסי עבודה מתקדמים ומומלצים
📌 useSelector - קריאה מה-Store
ה-Hook בשם useSelector מאפשר לקומפוננטה לקרוא נתון מתוך ה-store. הוא מקבל פונקציית "בורר" (selector) שבוחרת את החלק הרלוונטי מה-State, והקומפוננטה תתרנדר מחדש רק כשאותו חלק ישתנה. כדי לא לגרום לרינדורים מיותרים, עדיף לבחור ערכים ספציפיים ולא להחזיר אובייקט חדש בכל קריאה:
import { useSelector } from 'react-redux';
function MyComponent() {
// קריאה ישירה
const count = useSelector(state => state.counter.value);
// שימוש ב-selector מוגדר מראש
const todos = useSelector(selectAllTodos);
// Selector עם חישוב
const total = useSelector(state =>
state.cart.items.reduce((sum, item) => sum + item.price, 0)
);
// מספר ערכים (לא מומלץ - גורם לרינדורים מיותרים)
const { user, isLoading } = useSelector(state => ({
user: state.auth.user,
isLoading: state.auth.isLoading
}));
}
Selectors עם Reselect
import { createSelector } from '@reduxjs/toolkit';
// Selector בסיסי
const selectItems = state => state.cart.items;
const selectTaxRate = state => state.settings.taxRate;
// Selector מורכב עם memoization
const selectTotalWithTax = createSelector(
[selectItems, selectTaxRate],
(items, taxRate) => {
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return subtotal * (1 + taxRate);
}
);
// שימוש
function CartTotal() {
const total = useSelector(selectTotalWithTax);
return <p>סה"כ: ₪{total.toFixed(2)}</p>;
}
📌 useDispatch - שליחת Actions
אם useSelector משמש לקריאה מה-store, useDispatch משמש לכתיבה אליו. הוא מחזיר את פונקציית ה-dispatch, שבאמצעותה שולחים actions כדי לעדכן את ה-State:
import { useDispatch } from 'react-redux';
import { increment, addTodo } from './store';
function MyComponent() {
const dispatch = useDispatch();
// שליחה פשוטה
const handleClick = () => {
dispatch(increment());
};
// שליחה עם payload
const handleAdd = (text) => {
dispatch(addTodo(text));
};
// שליחה מרובה
const handleReset = () => {
dispatch(resetCart());
dispatch(clearUser());
dispatch(showNotification('הנתונים אופסו'));
};
}
📌 createAsyncThunk - פעולות אסינכרוניות
reducers רגילים חייבים להיות סינכרוניים, אך פעולות רבות (כמו שליפת נתונים מהשרת) לוקחות זמן. createAsyncThunk נועד בדיוק לכך: הוא עוטף פעולה אסינכרונית ומייצר עבורה שלושה actions אוטומטיים - pending (התחלה), fulfilled (הצלחה) ו-rejected (כישלון). את שלושתם מטפלים ב-slice דרך extraReducers, וכך מנהלים בקלות מצבי טעינה ושגיאה:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// יצירת async thunk
export const fetchUsers = createAsyncThunk(
'users/fetchUsers', // שם ה-action
async (_, { rejectWithValue }) => {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('שגיאה בטעינה');
}
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// עם פרמטרים
export const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`);
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// עם גישה ל-state
export const addToCart = createAsyncThunk(
'cart/addItem',
async (product, { getState, rejectWithValue }) => {
const { auth } = getState();
if (!auth.user) {
return rejectWithValue('יש להתחבר תחילה');
}
const response = await fetch('/api/cart', {
method: 'POST',
headers: {
'Authorization': `Bearer ${auth.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(product)
});
return await response.json();
}
);
טיפול ב-Slice
const usersSlice = createSlice({
name: 'users',
initialState: {
items: [],
selectedUser: null,
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null
},
reducers: {
clearError: (state) => {
state.error = null;
}
},
// טיפול ב-async actions
extraReducers: (builder) => {
builder
// fetchUsers
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || action.error.message;
})
// fetchUserById
.addCase(fetchUserById.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.status = 'succeeded';
state.selectedUser = action.payload;
})
.addCase(fetchUserById.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
});
}
});
שימוש בקומפוננטה
function UserList() {
const dispatch = useDispatch();
const { items: users, status, error } = useSelector(state => state.users);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchUsers());
}
}, [status, dispatch]);
if (status === 'loading') {
return <p>טוען משתמשים...</p>;
}
if (status === 'failed') {
return <p>שגיאה: {error}</p>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
📌 Middleware
Middleware הוא שכבת ביניים שכל action עובר דרכה בדרך אל ה-reducer. זו נקודת הידוק אידיאלית לפעולות רוחביות כמו רישום ללוג (logging), דיווח שגיאות או ניתוב פעולות אסינכרוניות:
// store/middleware/logger.js
const loggerMiddleware = (store) => (next) => (action) => {
console.group(action.type);
console.log('dispatching:', action);
console.log('prev state:', store.getState());
const result = next(action);
console.log('next state:', store.getState());
console.groupEnd();
return result;
};
// store/store.js
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: { /* ... */ },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware)
});
Middleware לדיווח שגיאות
const errorReportingMiddleware = (store) => (next) => (action) => {
try {
return next(action);
} catch (error) {
console.error('Error in reducer:', error);
// שליחה לשירות דיווח שגיאות
reportError(error, action);
throw error;
}
};
📌 Typed Hooks (TypeScript)
// store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Typed hooks
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// שימוש
function MyComponent() {
const dispatch = useAppDispatch();
const user = useAppSelector(state => state.auth.user); // עם autocomplete!
}
📌 דפוסי שימוש מתקדמים
Action Creator עם לוגיקה
// Thunk ידני (לא createAsyncThunk)
export const conditionalIncrement = () => (dispatch, getState) => {
const { value } = getState().counter;
if (value < 10) {
dispatch(increment());
}
};
export const incrementIfOdd = (amount) => (dispatch, getState) => {
const { value } = getState().counter;
if (value % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
Batch Updates
import { batch } from 'react-redux';
function handleSubmit() {
// כל ה-dispatches יגרמו לרינדור אחד בלבד
batch(() => {
dispatch(updateUser(userData));
dispatch(updateSettings(settings));
dispatch(showNotification('נשמר בהצלחה'));
});
}
📋 סיכום
| API | שימוש |
|---|---|
useSelector(selector) |
קריאה מה-store |
useDispatch() |
קבלת dispatch |
createAsyncThunk |
async actions |
extraReducers |
טיפול ב-thunk |
createSelector |
memoized selectors |
| Middleware | לוגיקה בין dispatch ל-reducer |
הקודם: ← Redux Basics
הבא: RTK Query →
🔄 RTK Query
מבוא
RTK Query הוא כלי ייעודי לשליפת נתונים (data fetching) ולניהול מטמון (caching), המגיע כחלק מ-Redux Toolkit. הוא הופך את הדפוס המוכר של useState + useEffect + createAsyncThunk למיותר: מגדירים את נקודות הקצה (endpoints) פעם אחת, ומקבלים Hooks מוכנים שמטפלים עבורכם באופן אוטומטי ב:
- מצבי טעינה ושגיאה (Loading / Error)
- שמירת תוצאות במטמון ושיתופן בין קומפוננטות (Caching)
- רענון נתונים אוטומטי בעת הצורך (Re-fetching)
- עדכונים אופטימיים (Optimistic updates)
📌 הגדרת API
נקודת המוצא היא הגדרת "פרוסת API" בעזרת createApi. בתוכה מגדירים את כתובת הבסיס של השרת ואת נקודות הקצה, המחולקות לשני סוגים: query לקריאת נתונים (GET) ו-mutation לשינוי נתונים (POST/PUT/DELETE). מנגנון ה-tags מאפשר ל-RTK Query לדעת אילו נתונים במטמון להתייתר ולרענן לאחר שינוי. מכל אלה נוצרים אוטומטית ה-Hooks שבהם נשתמש בקומפוננטות:
// store/api/usersApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const usersApi = createApi({
reducerPath: 'usersApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token;
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
}
}),
tagTypes: ['User', 'Users'],
endpoints: (builder) => ({
// GET all users
getUsers: builder.query({
query: () => '/users',
providesTags: ['Users']
}),
// GET single user
getUserById: builder.query({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }]
}),
// POST new user
addUser: builder.mutation({
query: (newUser) => ({
url: '/users',
method: 'POST',
body: newUser
}),
invalidatesTags: ['Users']
}),
// PUT update user
updateUser: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/users/${id}`,
method: 'PUT',
body: patch
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'User', id },
'Users'
]
}),
// DELETE user
deleteUser: builder.mutation({
query: (id) => ({
url: `/users/${id}`,
method: 'DELETE'
}),
invalidatesTags: ['Users']
})
})
});
// ייצוא Hooks אוטומטיים
export const {
useGetUsersQuery,
useGetUserByIdQuery,
useAddUserMutation,
useUpdateUserMutation,
useDeleteUserMutation
} = usersApi;
📌 הגדרת Store
// store/store.js
import { configureStore } from '@reduxjs/toolkit';
import { usersApi } from './api/usersApi';
const store = configureStore({
reducer: {
[usersApi.reducerPath]: usersApi.reducer,
// reducers נוספים...
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(usersApi.middleware)
});
export default store;
📌 שימוש - Query (GET)
ה-Hooks מסוג query משמשים לקריאת נתונים. הם מחזירים אובייקט עשיר הכולל את הנתונים עצמם (data), דגלי מצב (isLoading, isError) ופונקציית refetch לרענון ידני - כך שאין צורך לנהל את כל אלה בעצמנו:
import { useGetUsersQuery, useGetUserByIdQuery } from './store/api/usersApi';
function UserList() {
const {
data: users,
isLoading,
isError,
error,
isFetching,
refetch
} = useGetUsersQuery();
if (isLoading) return <p>טוען...</p>;
if (isError) return <p>שגיאה: {error.message}</p>;
return (
<div>
<button onClick={refetch} disabled={isFetching}>
{isFetching ? 'מרענן...' : 'רענן'}
</button>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
function UserProfile({ userId }) {
const { data: user, isLoading } = useGetUserByIdQuery(userId, {
// אפשרויות
skip: !userId, // דלג אם אין userId
pollingInterval: 30000, // רענון כל 30 שניות
refetchOnMountOrArgChange: true
});
if (isLoading) return <p>טוען פרופיל...</p>;
if (!user) return <p>משתמש לא נמצא</p>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
📌 שימוש - Mutation (POST/PUT/DELETE)
ה-Hooks מסוג mutation משמשים לשינוי נתונים בשרת. בשונה מ-query, הם אינם רצים אוטומטית אלא מחזירים פונקציית הפעלה שקוראים לה בעת הצורך. שרשור .unwrap() מאפשר לתפוס הצלחה או שגיאה בעזרת try/catch רגיל:
import {
useAddUserMutation,
useUpdateUserMutation,
useDeleteUserMutation
} from './store/api/usersApi';
function AddUserForm() {
const [addUser, { isLoading, isSuccess, isError, error }] = useAddUserMutation();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
await addUser({ name, email }).unwrap();
setName('');
setEmail('');
alert('המשתמש נוסף בהצלחה!');
} catch (err) {
alert('שגיאה: ' + err.message);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="שם"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="אימייל"
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'שומר...' : 'הוסף משתמש'}
</button>
</form>
);
}
function UserItem({ user }) {
const [updateUser] = useUpdateUserMutation();
const [deleteUser, { isLoading: isDeleting }] = useDeleteUserMutation();
const handleUpdate = async () => {
const newName = prompt('שם חדש:', user.name);
if (newName) {
await updateUser({ id: user.id, name: newName });
}
};
const handleDelete = async () => {
if (confirm('למחוק את המשתמש?')) {
await deleteUser(user.id);
}
};
return (
<li>
{user.name}
<button onClick={handleUpdate}>✏️ ערוך</button>
<button onClick={handleDelete} disabled={isDeleting}>
{isDeleting ? '...' : '❌ מחק'}
</button>
</li>
);
}
📌 Tags ו-Cache Invalidation
מנגנון ה-Tags הוא שמחבר בין קריאות לשינויים. כל query "מספקת" תגית (providesTags), וכל mutation יכולה "לבטל" תגית (invalidatesTags). כשמבטלים תגית, RTK Query מרענן אוטומטית כל query שסיפקה אותה - כך התצוגה נשארת מסונכרנת עם השרת ללא רענון ידני:
// Tags מאפשרים לקשר בין queries ל-mutations
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Posts'] // ה-query מספקת תגית 'Posts'
}),
addPost: builder.mutation({
query: (post) => ({ url: '/posts', method: 'POST', body: post }),
invalidatesTags: ['Posts'] // מבטלת את הקאש של 'Posts'
})
})
// כשנקרא addPost, getPosts יתרענן אוטומטית!
Tags מתקדמים
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Post', id })),
{ type: 'Post', id: 'LIST' }
]
: [{ type: 'Post', id: 'LIST' }]
}),
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
// מבטל רק את הפוסט הספציפי
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }]
})
})
📌 Optimistic Updates
בעדכון אופטימי מעדכנים את התצוגה מיד, עוד לפני שהשרת הספיק להשיב, כדי שהממשק ירגיש מהיר ותגובתי. אם השרת מחזיר שגיאה, מבטלים את השינוי וחוזרים למצב הקודם. RTK Query מספק לכך את onQueryStarted יחד עם undo:
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
// עדכון אופטימיסטי - לפני התשובה מהשרת
const patchResult = dispatch(
postsApi.util.updateQueryData('getPosts', undefined, (draft) => {
const post = draft.find(p => p.id === id);
if (post) {
Object.assign(post, patch);
}
})
);
try {
await queryFulfilled;
} catch {
// אם נכשל - חזור לערך הקודם
patchResult.undo();
}
}
})
📋 סיכום
| Hook | סוג | שימוש |
|---|---|---|
useGetXQuery |
Query | קריאת נתונים |
useXMutation |
Mutation | שינוי נתונים |
providesTags |
- | הגדרת תגיות קאש |
invalidatesTags |
- | ביטול קאש |
תכונות אוטומטיות
- ✅ Loading/Error states
- ✅ Caching
- ✅ Automatic refetch
- ✅ Deduplication
- ✅ Polling
- ✅ Optimistic updates
הקודם: ← Redux Hooks
הבא: Testing →
🧪 Testing - React Testing Library & Jest
מבוא
בדיקות אוטומטיות מעניקות ביטחון שהקוד ממשיך לעבוד גם אחרי שינויים. שני כלים משלימים מובילים בעולם ה-React: Jest - סביבת הרצת הבדיקות (test runner) שמריצה את הבדיקות ומספקת פונקציות השוואה (assertions), ו-React Testing Library (RTL) - ספרייה שבודקת קומפוננטות מנקודת מבטו של המשתמש, כלומר לפי מה שמוצג על המסך ולא לפי פרטי המימוש הפנימיים.
התקנה
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest
📌 מבנה בסיסי של בדיקה
// Component.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
test('renders correctly', () => {
render(<MyComponent />);
expect(screen.getByText('שלום')).toBeInTheDocument();
});
});
📌 render ו-screen
import { render, screen } from '@testing-library/react';
test('renders button with text', () => {
// render - מרנדר את הקומפוננטה
render(<Button>לחץ כאן</Button>);
// screen - גישה לאלמנטים שרונדרו
const button = screen.getByRole('button', { name: /לחץ כאן/i });
expect(button).toBeInTheDocument();
});
📌 Queries - חיפוש אלמנטים
סוגי Queries
| Query | מתי להשתמש |
|---|---|
getBy... |
האלמנט חייב להיות קיים |
queryBy... |
האלמנט עלול לא להיות קיים |
findBy... |
אלמנט אסינכרוני (Promise) |
שיטות חיפוש
// לפי Role (מומלץ!)
screen.getByRole('button');
screen.getByRole('heading', { level: 1 });
screen.getByRole('textbox', { name: /אימייל/i });
// לפי Label
screen.getByLabelText('סיסמה');
// לפי Placeholder
screen.getByPlaceholderText('הקלד כאן...');
// לפי טקסט
screen.getByText('שלום עולם');
screen.getByText(/שלום/i); // case-insensitive
// לפי Test ID
screen.getByTestId('submit-button');
// לפי Alt
screen.getByAltText('לוגו החברה');
דוגמאות
test('form elements', () => {
render(<LoginForm />);
// getBy - זורק שגיאה אם לא נמצא
const emailInput = screen.getByLabelText(/אימייל/i);
const passwordInput = screen.getByLabelText(/סיסמה/i);
const submitButton = screen.getByRole('button', { name: /התחבר/i });
// queryBy - מחזיר null אם לא נמצא
const error = screen.queryByText(/שגיאה/i);
expect(error).not.toBeInTheDocument();
});
📌 User Events - אינטראקציות
הספרייה user-event מדמה אינטראקציות אמיתיות של משתמש - הקלדה, לחיצה, בחירה מרשימה ועוד - בצורה קרובה יותר להתנהגות בדפדפן. מכיוון שהפעולות אסינכרוניות, מקדימים להן await:
import userEvent from '@testing-library/user-event';
test('user interactions', async () => {
const user = userEvent.setup();
render(<MyForm onSubmit={handleSubmit} />);
// הקלדה
await user.type(screen.getByLabelText(/שם/i), 'דני');
// לחיצה
await user.click(screen.getByRole('button'));
// Clear + type
await user.clear(screen.getByLabelText(/שם/i));
await user.type(screen.getByLabelText(/שם/i), 'רונית');
// Hover
await user.hover(screen.getByText('עזרה'));
// Select
await user.selectOptions(screen.getByRole('combobox'), 'option1');
// Checkbox
await user.click(screen.getByRole('checkbox'));
});
📌 Assertions - בדיקות
import '@testing-library/jest-dom';
test('assertions', () => {
render(<MyComponent />);
const element = screen.getByText('שלום');
// נוכחות
expect(element).toBeInTheDocument();
expect(screen.queryByText('לא קיים')).not.toBeInTheDocument();
// תוכן
expect(element).toHaveTextContent('שלום');
expect(screen.getByLabelText('שם')).toHaveValue('דני');
// מצב
expect(screen.getByRole('button')).toBeEnabled();
expect(screen.getByRole('button')).toBeDisabled();
// Style
expect(element).toHaveClass('active');
expect(element).toHaveStyle({ color: 'red' });
// Attributes
expect(element).toHaveAttribute('href', '/about');
});
📝 דוגמאות מלאות
בדיקת קומפוננטה פשוטה
// Greeting.jsx
function Greeting({ name }) {
return <h1>שלום, {name}!</h1>;
}
// Greeting.test.jsx
test('renders greeting with name', () => {
render(<Greeting name="דני" />);
expect(screen.getByRole('heading')).toHaveTextContent('שלום, דני!');
});
test('renders greeting with different name', () => {
render(<Greeting name="רונית" />);
expect(screen.getByText(/שלום, רונית/i)).toBeInTheDocument();
});
בדיקת Counter
// Counter.jsx
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p data-testid="count">{count}</p>
<button onClick={() => setCount(c => c + 1)}>הוסף</button>
<button onClick={() => setCount(c => c - 1)}>הפחת</button>
</div>
);
}
// Counter.test.jsx
describe('Counter', () => {
test('starts at 0', () => {
render(<Counter />);
expect(screen.getByTestId('count')).toHaveTextContent('0');
});
test('increments on button click', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: /הוסף/i }));
expect(screen.getByTestId('count')).toHaveTextContent('1');
});
test('decrements on button click', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: /הפחת/i }));
expect(screen.getByTestId('count')).toHaveTextContent('-1');
});
});
בדיקת טופס
// LoginForm.test.jsx
describe('LoginForm', () => {
const mockSubmit = jest.fn();
beforeEach(() => {
mockSubmit.mockClear();
});
test('submits form with valid data', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText(/אימייל/i), '[email protected]');
await user.type(screen.getByLabelText(/סיסמה/i), '123456');
await user.click(screen.getByRole('button', { name: /התחבר/i }));
expect(mockSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: '123456'
});
});
test('shows error for invalid email', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText(/אימייל/i), 'invalid');
await user.click(screen.getByRole('button', { name: /התחבר/i }));
expect(screen.getByText(/אימייל לא תקין/i)).toBeInTheDocument();
expect(mockSubmit).not.toHaveBeenCalled();
});
});
📌 בדיקות אסינכרוניות
// async component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <p>טוען...</p>;
return <h1>{user.name}</h1>;
}
// test
test('loads and displays user', async () => {
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'דני' })
})
);
render(<UserProfile userId="1" />);
// בהתחלה מציג loading
expect(screen.getByText(/טוען/i)).toBeInTheDocument();
// מחכה לתוכן האסינכרוני
const heading = await screen.findByRole('heading');
expect(heading).toHaveTextContent('דני');
});
📌 Mocking
לעיתים קרובות נרצה לבודד את הקומפוננטה הנבדקת מהתלויות החיצוניות שלה - למשל קריאות לשרת או פונקציות שמועברות כ-props. לשם כך משתמשים ב-Mocks: תחליפים מזויפים שמאפשרים לשלוט בהתנהגות ולוודא שהקומפוננטה קראה להם כצפוי:
Mock Functions
test('calls onClick when clicked', async () => {
const handleClick = jest.fn();
const user = userEvent.setup();
render(<Button onClick={handleClick}>לחץ</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
Mock Modules
// __mocks__/api.js
export const fetchUsers = jest.fn();
// Component.test.jsx
jest.mock('./api');
import { fetchUsers } from './api';
beforeEach(() => {
fetchUsers.mockResolvedValue([
{ id: 1, name: 'דני' }
]);
});
test('displays fetched users', async () => {
render(<UserList />);
expect(await screen.findByText('דני')).toBeInTheDocument();
});
📋 Best Practices
- בדקו התנהגות, לא מימוש - התמקדו במה שהמשתמש רואה ועושה, ולא בפרטים הפנימיים של הקוד
- העדיפו חיפוש לפי Role - הוא עמיד יותר ומקדם נגישות, בניגוד ל-
test-id - כתבו בדיקות שמדמות שימוש אמיתי - בדיקה טובה משקפת את דרך העבודה של משתמש חי
- השתמשו ב-
userEventולא ב-fireEvent- הוא מדמה אינטראקציות נאמנה יותר למציאות
📋 סיכום
| API | שימוש |
|---|---|
render() |
רינדור קומפוננטה |
screen |
גישה לאלמנטים |
getBy/queryBy/findBy |
חיפוש אלמנטים |
userEvent |
סימולציית אינטראקציות |
expect() |
assertions |
jest.fn() |
mock function |
הקודם: ← RTK Query
סיום! 🎉
📱 React Native עם Expo - מדריך מקיף
מה זה React Native?
React Native הוא פריימוורק של Meta (Facebook) לפיתוח אפליקציות מובייל Native לאנדרואיד ול-iOS, באמצעות אותם JavaScript ו-React שכבר הכרנו. היתרון הגדול: במקום ללמוד שפות שונות ולתחזק שתי אפליקציות נפרדות, כותבים בסיס קוד אחד שמתורגם לרכיבי ממשק מקוריים בכל פלטפורמה - לא אתר עטוף (WebView), אלא אפליקציה אמיתית לכל דבר.
React (Web) → HTML Elements → דפדפן
React Native (Mobile) → Native Components → iOS / Android
יתרונות React Native
| יתרון | הסבר |
|---|---|
| קוד אחד | אותו קוד לשתי הפלטפורמות |
| React | אותו תחביר ועקרונות מ-React |
| Native | UI אמיתי, לא WebView |
| Hot Reload | רענון מהיר בפיתוח |
| קהילה גדולה | הרבה ספריות וכלים |
| ביצועים | קרוב ל-Native אמיתי |
🚀 מה זה Expo?
Expo הוא מסגרת עבודה ומערכת כלים שנבנו סביב React Native כדי לפשט מאוד את הפיתוח. במקום להתמודד עם הגדרת סביבות מורכבות ב-Android Studio וב-Xcode, Expo מטפל בכל התשתית עבורכם - כך שאפשר להתחיל לפתח ולראות תוצאות על מכשיר אמיתי תוך דקות. זו נקודת הפתיחה המומלצת ביותר למי שמתחיל עם React Native.
השוואה: Expo vs React Native CLI
Expo React Native CLI
==== ================
התקנה: קלה מאוד מורכבת
זמן הקמה: דקות שעות
דרישות: Node.js בלבד Xcode, Android Studio
Native Modules: מוגבל* מלא
גודל APK: גדול יותר קטן יותר
למתחילים: מומלץ מאוד מאתגר
* Expo Go מוגבל, אבל EAS Build מאפשר הכל
סוגי Workflows ב-Expo
1. Managed Workflow (מנוהל)
- Expo מנהל את הכל
- אין גישה לקוד Native
- הכי פשוט למתחילים
2. Bare Workflow (חשוף)
- גישה מלאה לקוד Native
- יותר שליטה
- דומה ל-React Native CLI
🛠️ התקנה והגדרה
1. התקנת Node.js
# בדיקה שיש Node.js
node --version # צריך 18+
npm --version
2. התקנת Expo CLI
# אין צורך להתקין גלובלית יותר, משתמשים ב-npx
npx create-expo-app@latest
3. יצירת פרויקט חדש
# יצירת פרויקט
npx create-expo-app@latest my-app
# או עם template
npx create-expo-app@latest my-app --template blank
npx create-expo-app@latest my-app --template blank-typescript
npx create-expo-app@latest my-app --template tabs
# כניסה לפרויקט
cd my-app
4. הרצת הפרויקט
# הרצה
npx expo start
# או
npm start
5. צפייה באפליקציה
- Expo Go App: הורידו מה-App Store או Google Play
- סרקו את ה-QR Code מהטרמינל
- או לחצו
aלאנדרואיד /iל-iOS (דורש אמולטור)
📁 מבנה פרויקט Expo
my-app/
├── app/ # App Router (Expo Router)
│ ├── (tabs)/ # קבוצת טאבים
│ │ ├── index.tsx # מסך ראשי
│ │ └── explore.tsx # מסך נוסף
│ ├── _layout.tsx # Layout ראשי
│ └── +not-found.tsx # דף 404
├── assets/ # תמונות, פונטים, וכו'
│ ├── images/
│ └── fonts/
├── components/ # רכיבים משותפים
├── constants/ # קבועים
├── hooks/ # Custom Hooks
├── app.json # הגדרות האפליקציה
├── package.json # תלויות
├── tsconfig.json # הגדרות TypeScript
└── babel.config.js # הגדרות Babel
🧩 רכיבים בסיסיים
ב-React Native אין תגיות HTML כמו div או p. במקומן משתמשים ברכיבים מובנים שמתורגמים לרכיבי ממשק מקוריים. אלו אבני הבניין שתפגשו כמעט בכל מסך:
View - הקונטיינר הבסיסי (כמו div)
import { View, StyleSheet } from 'react-native';
export default function MyComponent() {
return (
<View style={styles.container}>
{/* תוכן */}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
padding: 20,
},
});
Text - טקסט (חובה לכל טקסט!)
import { Text, View } from 'react-native';
export default function TextExample() {
return (
<View>
{/* ❌ לא עובד ב-React Native */}
{/* <View>שלום עולם</View> */}
{/* ✅ נכון */}
<Text>שלום עולם!</Text>
<Text style={{ fontSize: 24, fontWeight: 'bold', color: 'blue' }}>
כותרת גדולה
</Text>
{/* Text מקונן */}
<Text>
טקסט רגיל <Text style={{ fontWeight: 'bold' }}>מודגש</Text> ועוד
</Text>
</View>
);
}
Image - תמונות
import { Image, View } from 'react-native';
export default function ImageExample() {
return (
<View>
{/* תמונה מקומית */}
<Image
source={require('./assets/logo.png')}
style={{ width: 100, height: 100 }}
/>
{/* תמונה מה-URL */}
<Image
source={{ uri: 'https://example.com/image.jpg' }}
style={{ width: 200, height: 200 }}
resizeMode="cover" // cover, contain, stretch, center
/>
</View>
);
}
Button ו-Pressable
import { Button, Pressable, Text, View, Alert } from 'react-native';
export default function ButtonExample() {
const handlePress = () => {
Alert.alert('לחצת!', 'הכפתור עובד');
};
return (
<View>
{/* Button בסיסי (מוגבל בסגנון) */}
<Button
title="לחץ כאן"
onPress={handlePress}
color="#841584"
/>
{/* Pressable - יותר גמיש */}
<Pressable
onPress={handlePress}
style={({ pressed }) => [
{
backgroundColor: pressed ? '#ddd' : '#007AFF',
padding: 15,
borderRadius: 10,
marginTop: 10,
}
]}
>
<Text style={{ color: 'white', textAlign: 'center' }}>
כפתור מותאם
</Text>
</Pressable>
</View>
);
}
TextInput - שדה קלט
import { useState } from 'react';
import { TextInput, View, Text, StyleSheet } from 'react-native';
export default function InputExample() {
const [text, setText] = useState('');
const [password, setPassword] = useState('');
return (
<View style={styles.container}>
{/* שדה טקסט רגיל */}
<TextInput
style={styles.input}
placeholder="הכנס שם"
value={text}
onChangeText={setText}
/>
{/* שדה סיסמה */}
<TextInput
style={styles.input}
placeholder="סיסמה"
value={password}
onChangeText={setPassword}
secureTextEntry={true}
/>
{/* שדה מספרי */}
<TextInput
style={styles.input}
placeholder="מספר טלפון"
keyboardType="phone-pad"
/>
{/* שדה אימייל */}
<TextInput
style={styles.input}
placeholder="אימייל"
keyboardType="email-address"
autoCapitalize="none"
/>
<Text>הקלדת: {text}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 20 },
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
marginBottom: 10,
fontSize: 16,
},
});
ScrollView - גלילה
import { ScrollView, View, Text, StyleSheet } from 'react-native';
export default function ScrollExample() {
return (
<ScrollView style={styles.container}>
{[...Array(20)].map((_, i) => (
<View key={i} style={styles.item}>
<Text>פריט מספר {i + 1}</Text>
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
item: {
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
});
FlatList - רשימה (יעיל לרשימות ארוכות!)
import { FlatList, View, Text, StyleSheet } from 'react-native';
const DATA = [
{ id: '1', title: 'פריט ראשון' },
{ id: '2', title: 'פריט שני' },
{ id: '3', title: 'פריט שלישי' },
// ... עוד הרבה פריטים
];
export default function ListExample() {
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text>{item.title}</Text>
</View>
);
return (
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListHeaderComponent={() => <Text style={styles.header}>הרשימה שלי</Text>}
ListEmptyComponent={() => <Text>אין פריטים</Text>}
/>
);
}
const styles = StyleSheet.create({
item: { padding: 20 },
separator: { height: 1, backgroundColor: '#eee' },
header: { fontSize: 24, padding: 20, fontWeight: 'bold' },
});
🎨 עיצוב ב-React Native
העיצוב ב-React Native דומה ל-CSS אך אינו זהה לו: כותבים אותו כאובייקטים של JavaScript (בדרך כלל דרך StyleSheet.create), שמות המאפיינים ב-camelCase, והפריסה מבוססת כולה על Flexbox. שימו לב לכמה הבדלים מהותיים: הערכים הם מספרים ולא מחרוזות עם px, ו-flexDirection הוא column כברירת מחדל (בניגוד ל-row בדפדפן):
StyleSheet
import { StyleSheet, View, Text } from 'react-native';
export default function StyledComponent() {
return (
<View style={styles.container}>
<Text style={styles.title}>כותרת</Text>
<Text style={[styles.text, styles.highlight]}>טקסט מודגש</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 20,
// אין margin: auto, אין display: grid
// משתמשים ב-Flexbox!
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#333',
marginBottom: 10,
},
text: {
fontSize: 16,
color: '#666',
lineHeight: 24,
},
highlight: {
backgroundColor: 'yellow',
padding: 5,
},
});
Flexbox (ברירת המחדל!)
const styles = StyleSheet.create({
// עמודה (ברירת מחדל - שונה מ-Web!)
column: {
flexDirection: 'column', // ברירת מחדל
},
// שורה
row: {
flexDirection: 'row',
},
// מרכוז
centered: {
justifyContent: 'center', // ציר ראשי
alignItems: 'center', // ציר משני
},
// חלוקת מרחב
spaceBetween: {
justifyContent: 'space-between',
},
// גמישות
flexible: {
flex: 1, // תופס את כל המקום הזמין
},
});
הבדלים מ-CSS
// ❌ לא עובד ב-React Native
// margin: '10px'
// font-size: 16px
// background-color: red
// ✅ נכון ב-React Native
{
margin: 10, // מספר בלבד (pixels)
fontSize: 16, // camelCase
backgroundColor: 'red',
// ❌ אין:
// display: 'block' / 'inline'
// float
// position: 'fixed'
// ✅ יש:
// position: 'absolute' / 'relative'
// flexbox (ברירת מחדל)
}
🧭 ניווט עם Expo Router
Expo Router מיישם ניתוב מבוסס קבצים (File-based routing), בדומה ל-Next.js: מבנה התיקיות בתוך תיקיית app הוא שמגדיר את הנתיבים באפליקציה. כך אין צורך להגדיר נתיבים ידנית - כל קובץ הופך אוטומטית למסך:
התקנה
npx expo install expo-router expo-linking expo-constants expo-status-bar
מבנה תיקיות
app/
├── _layout.tsx # Layout ראשי
├── index.tsx # דף הבית (/)
├── about.tsx # דף אודות (/about)
├── profile/
│ ├── index.tsx # /profile
│ └── [id].tsx # /profile/123 (דינמי)
└── (tabs)/ # קבוצת טאבים
├── _layout.tsx # Layout לטאבים
├── home.tsx # Tab 1
└── settings.tsx # Tab 2
Layout בסיסי
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{ title: 'דף הבית' }}
/>
<Stack.Screen
name="about"
options={{ title: 'אודות' }}
/>
</Stack>
);
}
ניווט בין מסכים
// app/index.tsx
import { Link, router } from 'expo-router';
import { View, Text, Pressable } from 'react-native';
export default function HomeScreen() {
return (
<View>
{/* Link (דומה לאנכור) */}
<Link href="/about">
<Text>עבור לאודות</Text>
</Link>
{/* ניווט פרוגרמטי */}
<Pressable onPress={() => router.push('/profile/123')}>
<Text>עבור לפרופיל</Text>
</Pressable>
{/* חזרה */}
<Pressable onPress={() => router.back()}>
<Text>חזור</Text>
</Pressable>
</View>
);
}
קבלת פרמטרים
// app/profile/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';
export default function ProfileScreen() {
const { id } = useLocalSearchParams();
return (
<View>
<Text>פרופיל מספר: {id}</Text>
</View>
);
}
Tab Navigation
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="home"
options={{
title: 'בית',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'הגדרות',
tabBarIcon: ({ color, size }) => (
<Ionicons name="settings" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
📦 APIs נפוצים של Expo
אחד היתרונות הגדולים של Expo הוא אוסף המודולים המובנים לגישה ליכולות המכשיר - אחסון, מצלמה, מיקום, התראות ועוד. כל אחד מהם מותקן בפקודה אחת ומספק API נקי ואחיד לשתי הפלטפורמות. הנה כמה מהנפוצים ביותר:
AsyncStorage - אחסון מקומי
npx expo install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
// שמירה
await AsyncStorage.setItem('user', JSON.stringify({ name: 'דוד' }));
// קריאה
const userJson = await AsyncStorage.getItem('user');
const user = userJson ? JSON.parse(userJson) : null;
// מחיקה
await AsyncStorage.removeItem('user');
SecureStore - אחסון מאובטח
npx expo install expo-secure-store
import * as SecureStore from 'expo-secure-store';
// שמירה
await SecureStore.setItemAsync('token', 'abc123');
// קריאה
const token = await SecureStore.getItemAsync('token');
// מחיקה
await SecureStore.deleteItemAsync('token');
Camera - מצלמה
npx expo install expo-camera
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useState } from 'react';
import { View, Button, Text } from 'react-native';
export default function CameraExample() {
const [permission, requestPermission] = useCameraPermissions();
if (!permission) {
return <View />;
}
if (!permission.granted) {
return (
<View>
<Text>צריך הרשאה למצלמה</Text>
<Button onPress={requestPermission} title="תן הרשאה" />
</View>
);
}
return (
<CameraView style={{ flex: 1 }} facing="back">
{/* כפתורי שליטה */}
</CameraView>
);
}
Location - מיקום
npx expo install expo-location
import * as Location from 'expo-location';
import { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
export default function LocationExample() {
const [location, setLocation] = useState(null);
useEffect(() => {
(async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
return;
}
const loc = await Location.getCurrentPositionAsync({});
setLocation(loc);
})();
}, []);
return (
<View>
{location && (
<Text>
מיקום: {location.coords.latitude}, {location.coords.longitude}
</Text>
)}
</View>
);
}
Notifications - התראות
npx expo install expo-notifications expo-device
import * as Notifications from 'expo-notifications';
import { useEffect } from 'react';
// הגדרת התנהגות כשמתקבלת התראה
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
export default function NotificationExample() {
useEffect(() => {
registerForPushNotificationsAsync();
}, []);
// שליחת התראה מקומית
const sendLocalNotification = async () => {
await Notifications.scheduleNotificationAsync({
content: {
title: "שלום!",
body: "זו התראה מהאפליקציה",
},
trigger: { seconds: 2 },
});
};
return (/* ... */);
}
🏗️ בניה והפצה
כשהאפליקציה מוכנה, יש להפוך אותה לקובץ התקנה (APK או IPA) ולהעלות אותה לחנויות. Expo מספק לכך שירות ענן בשם EAS (Expo Application Services), שבונה את האפליקציה עבורכם ללא צורך ב-Xcode או Android Studio מקומיים:
EAS Build - בניית אפליקציה
# התקנת EAS CLI
npm install -g eas-cli
# התחברות
eas login
# הגדרה ראשונית
eas build:configure
# בנייה לאנדרואיד
eas build --platform android
# בנייה ל-iOS
eas build --platform ios
# בנייה לשניהם
eas build --platform all
סוגי Build
# Development build (לפיתוח)
eas build --profile development --platform android
# Preview build (לבדיקות)
eas build --profile preview --platform android
# Production build (להפצה)
eas build --profile production --platform android
EAS Submit - העלאה לחנויות
# העלאה ל-Google Play
eas submit --platform android
# העלאה ל-App Store
eas submit --platform ios
📝 דוגמה מלאה - אפליקציית Todo
// app/index.tsx
import { useState } from 'react';
import {
View,
Text,
TextInput,
FlatList,
Pressable,
StyleSheet,
} from 'react-native';
interface Todo {
id: string;
text: string;
completed: boolean;
}
export default function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputText, setInputText] = useState('');
const addTodo = () => {
if (inputText.trim()) {
setTodos([
...todos,
{ id: Date.now().toString(), text: inputText, completed: false },
]);
setInputText('');
}
};
const toggleTodo = (id: string) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id: string) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
return (
<View style={styles.container}>
<Text style={styles.title}>📝 המשימות שלי</Text>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="הוסף משימה..."
value={inputText}
onChangeText={setInputText}
onSubmitEditing={addTodo}
/>
<Pressable style={styles.addButton} onPress={addTodo}>
<Text style={styles.addButtonText}>+</Text>
</Pressable>
</View>
<FlatList
data={todos}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.todoItem}>
<Pressable
style={styles.checkbox}
onPress={() => toggleTodo(item.id)}
>
<Text>{item.completed ? '✅' : '⬜'}</Text>
</Pressable>
<Text
style={[
styles.todoText,
item.completed && styles.completedText,
]}
>
{item.text}
</Text>
<Pressable onPress={() => deleteTodo(item.id)}>
<Text style={styles.deleteButton}>🗑️</Text>
</Pressable>
</View>
)}
ListEmptyComponent={
<Text style={styles.emptyText}>אין משימות. הוסף אחת!</Text>
}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 20,
paddingTop: 60,
},
title: {
fontSize: 28,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
},
inputContainer: {
flexDirection: 'row',
marginBottom: 20,
},
input: {
flex: 1,
backgroundColor: 'white',
padding: 15,
borderRadius: 10,
fontSize: 16,
marginRight: 10,
},
addButton: {
backgroundColor: '#007AFF',
width: 50,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
addButtonText: {
color: 'white',
fontSize: 24,
fontWeight: 'bold',
},
todoItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
padding: 15,
borderRadius: 10,
marginBottom: 10,
},
checkbox: {
marginRight: 10,
},
todoText: {
flex: 1,
fontSize: 16,
},
completedText: {
textDecorationLine: 'line-through',
color: '#888',
},
deleteButton: {
fontSize: 20,
},
emptyText: {
textAlign: 'center',
color: '#888',
marginTop: 50,
},
});
📚 משאבים נוספים
📝 סיכום
| נושא | טכנולוגיה |
|---|---|
| פלטפורמה | Expo |
| ניווט | Expo Router |
| סגנונות | StyleSheet + Flexbox |
| אחסון | AsyncStorage / SecureStore |
| בנייה | EAS Build |
| הפצה | EAS Submit |
בהצלחה! 🎉