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 Toolkit על Redux הישן?
הצגת התשובה
תשובה ד. createSlice משתמש ב-Immer, כך ש-state.value++ בטוח.
-
מה זה slice ב-Redux Toolkit?
הצגת התשובה
תשובה ב. כל תחום (משתמש, עגלה) מקבל slice משלו.