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 →

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. מה היתרון של Redux Toolkit על Redux הישן?

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

    תשובה ד. createSlice משתמש ב-Immer, כך ש-state.value++ בטוח.

  2. מה זה slice ב-Redux Toolkit?

    1. קובץ CSS
    2. חלק מה-state עם ה-reducers וה-actions שלו
    3. בקשת רשת
    4. קומפוננטה
    הצגת התשובה

    תשובה ב. כל תחום (משתמש, עגלה) מקבל slice משלו.