הגדרת ניהול מצב (Redux) עבור יישומי React
אי-התאמת Hydration ב-SSR, תנאי מרוץ עם async thunks, ו-re-renders בלתי צפויים — אלה הם תסמינים מוכרים של כאוס בניהול מצב. בפרויקטים עם עשרות רכיבים ומאות מצבים, בעיות אלה מגדילות את זמן הפיתוח ב-30–50%. Redux Toolkit פותר אותן מבחינה ארכיטקטונית: זרימת נתונים חד-כיוונית, פונקציות reducer טהורות, ואובייקטי פעולה מפורשים. בפרויקט אחד עם 15 מפתחים ויותר מ-200 מסכים, המעבר מ-Redux 3 ל-Redux Toolkit הפחית את מספר הבאגים ב-40% וקיצר את זמן יישום התכונות החדשות בחצי. החיסכון בניפוי באגים הסתכם בכ-$12,000 לרבעון, ולצוות של 10 מפתחים, זה אומר יותר מ-$100,000 שנחסכים מדי שנה.
אנו מגדירים Redux עם מחסנית מודרנית: Redux Toolkit כדי לבטל boilerplate (מפחית קוד ב-70%), RTK Query למצב שרת (caching אוטומטי), ו-Redux DevTools לניפוי באגים עם time-travel. אנו משתמשים ב-hooks טיפוסיים useAppDispatch ו-useAppSelector, שמבטלים שגיאות טיפוס ב-98% מהמקרים.
השוואה בין גישות ניהול מצב:
| קריטריון | useState | Context | Redux Toolkit |
|---|---|---|---|
| מצב גלובלי | לא | כן, אבל ללא DevTools | כן, עם ניפוי באגים מלא |
| Selectors עם memoization | לא | לא | createSelector |
| Middleware | לא | לא | כן (thunk, saga) |
| Time-travel | לא | לא | כן |
| Boilerplate | מינימלי | בינוני | בינוני (70% פחות מ-Redux קלאסי) |
Redux Toolkit מפחית boilerplate פי 3 בהשוואה ל-Redux קלאסי, ו-RTK Query מייצר פי 3 פחות קוד מאשר createAsyncThunk ידני.
כיצד לבנות נכון את ה-store?
המבנה המומלץ מפצל את ה-store לפי תחומים:
src/
store/
index.ts # Конфигурация store
hooks.ts # Типизированные useAppDispatch, useAppSelector
slices/
authSlice.ts
cartSlice.ts
uiSlice.ts
api/
productsApi.ts # RTK Query endpoints // store/index.ts и store/hooks.ts — объединено для краткости
import { configureStore } from '@reduxjs/toolkit';
import { authSlice } from './slices/authSlice';
import { cartSlice } from './slices/cartSlice';
import { uiSlice } from './slices/uiSlice';
import { productsApi } from './api/productsApi';
export const store = configureStore({
reducer: {
auth: authSlice.reducer,
cart: cartSlice.reducer,
ui: uiSlice.reducer,
[productsApi.reducerPath]: productsApi.reducer,
},
middleware: (getDefault) => getDefault().concat(productsApi.middleware),
devTools: process.env.NODE_ENV !== 'production' && {
name: 'MyApp',
trace: true,
traceLimit: 25,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Типизированные хуки
import { useDispatch, useSelector } from 'react-redux';
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
Slice עם לוגיקה עסקית
// store/slices/cartSlice.ts
import { createSlice, createSelector, type PayloadAction } from '@reduxjs/toolkit';
interface CartItem {
id: string;
name: string;
price: number;
qty: number;
image: string;
}
interface CartState {
items: CartItem[];
coupon: string | null;
couponDiscount: number;
}
const initialState: CartState = {
items: [],
coupon: null,
couponDiscount: 0,
};
export const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem(state, action: PayloadAction<Omit<CartItem, 'qty'>>) {
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) {
existing.qty += 1;
} else {
state.items.push({ ...action.payload, qty: 1 });
}
},
removeItem(state, action: PayloadAction<string>) {
state.items = state.items.filter(i => i.id !== action.payload);
},
updateQty(state, action: PayloadAction<{ id: string; qty: number }>) {
const item = state.items.find(i => i.id === action.payload.id);
if (item) {
item.qty = Math.max(1, action.payload.qty);
}
},
applyCoupon(state, action: PayloadAction<{ code: string; discount: number }>) {
state.coupon = action.payload.code;
state.couponDiscount = action.payload.discount;
},
clearCart(state) {
state.items = [];
state.coupon = null;
state.couponDiscount = 0;
},
},
});
// Мемоизированные селекторы
const selectCartItems = (state: RootState) => state.cart.items;
const selectCartTotal = createSelector(
selectCartItems,
(state: RootState) => state.cart.couponDiscount,
(items, discount) => {
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return subtotal * (1 - discount / 100);
}
);
const selectCartCount = createSelector(
selectCartItems,
items => items.reduce((sum, item) => sum + item.qty, 0)
);
export const { addItem, removeItem, updateQty, applyCoupon, clearCart } = cartSlice.actions;
כיצד להימנע מ-re-renders מיותרים בעת שימוש ב-Redux?
הגורם העיקרי הוא רכיב שנרשם לחתיכת מצב גדולה מדי. פתרון: selectors עם memoization מ-createSelector. הם מחשבים מחדש רק כאשר חלקים תלויים משתנים, ומפחיתים re-renders ב-60%. בנוסף, השתמשו ב-src/ store/ index.ts # Конфигурация store hooks.ts # Типизированные useAppDispatch, useAppSelector slices/ authSlice.ts cartSlice.ts uiSlice.ts api/ productsApi.ts # RTK Query endpoints ו-// store/index.ts и store/hooks.ts — объединено для краткости import { configureStore } from '@reduxjs/toolkit'; import { authSlice } from './slices/authSlice'; import { cartSlice } from './slices/cartSlice'; import { uiSlice } from './slices/uiSlice'; import { productsApi } from './api/productsApi'; export const store = configureStore({ reducer: { auth: authSlice.reducer, cart: cartSlice.reducer, ui: uiSlice.reducer, [productsApi.reducerPath]: productsApi.reducer, }, middleware: (getDefault) => getDefault().concat(productsApi.middleware), devTools: process.env.NODE_ENV !== 'production' && { name: 'MyApp', trace: true, traceLimit: 25, }, }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; // Типизированные хуки import { useDispatch, useSelector } from 'react-redux'; export const useAppDispatch = useDispatch.withTypes<AppDispatch>(); export const useAppSelector = useSelector.withTypes<RootState>(); בתוך // store/slices/cartSlice.ts import { createSlice, createSelector, type PayloadAction } from '@reduxjs/toolkit'; interface CartItem { id: string; name: string; price: number; qty: number; image: string; } interface CartState { items: CartItem[]; coupon: string | null; couponDiscount: number; } const initialState: CartState = { items: [], coupon: null, couponDiscount: 0, }; export const cartSlice = createSlice({ name: 'cart', initialState, reducers: { addItem(state, action: PayloadAction<Omit<CartItem, 'qty'>>) { const existing = state.items.find(i => i.id === action.payload.id); if (existing) { existing.qty += 1; } else { state.items.push({ ...action.payload, qty: 1 }); } }, removeItem(state, action: PayloadAction<string>) { state.items = state.items.filter(i => i.id !== action.payload); }, updateQty(state, action: PayloadAction<{ id: string; qty: number }>) { const item = state.items.find(i => i.id === action.payload.id); if (item) { item.qty = Math.max(1, action.payload.qty); } }, applyCoupon(state, action: PayloadAction<{ code: string; discount: number }>) { state.coupon = action.payload.code; state.couponDiscount = action.payload.discount; }, clearCart(state) { state.items = []; state.coupon = null; state.couponDiscount = 0; }, }, }); // Мемоизированные селекторы const selectCartItems = (state: RootState) => state.cart.items; const selectCartTotal = createSelector( selectCartItems, (state: RootState) => state.cart.couponDiscount, (items, discount) => { const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0); return subtotal * (1 - discount / 100); } ); const selectCartCount = createSelector( selectCartItems, items => items.reduce((sum, item) => sum + item.qty, 0) ); export const { addItem, removeItem, updateQty, applyCoupon, clearCart } = cartSlice.actions; . בדוגמה שלנו, React.memo תלוי רק ב-items ו-discount — שינויים בשדות אחרים לא יגרמו לציור מחדש.
הגדרה שלב-אחר-שלב של Redux Toolkit
- התקנת חבילות:
shallowEqual. - יצירת
useSelectorוהגדרת ה-store עםselectCartTotal. - עבור כל תחום, צרו slice באמצעות
npm install @reduxjs/toolkit react-redux. - ייצוא hooks טיפוסיים
store/index.tsו-configureStore. - לקריאות API, השתמשו ב-
createSliceמ-RTK Query. - חיבור ה-store לאפליקציה דרך
useAppDispatch.
RTK Query למצב שרת
RTK Query הופך caching, revalidation ועדכוני נתוני שרת לאוטומטיים. השוואה לניהול ידני: RTK Query מפחית קוד עבור CRUD טיפוסי ב-60-80%. בפרויקטים אמיתיים, זה אומר פי 3 פחות שורות קוד ו-50% פחות זמן בתחזוקת שכבת ה-API.
// store/api/productsApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) headers.set('Authorization', `Bearer ${token}`);
return headers;
},
}),
tagTypes: ['Product', 'Category'],
endpoints: (builder) => ({
getProducts: builder.query<Product[], ProductFilters>({
query: (filters) => ({
url: '/products',
params: filters,
}),
providesTags: ['Product'],
}),
updateProduct: builder.mutation<Product, Partial<Product> & { id: string }>({
query: ({ id, ...body }) => ({
url: `/products/${id}`,
method: 'PUT',
body,
}),
invalidatesTags: ['Product'],
}),
}),
});
export const { useGetProductsQuery, useUpdateProductMutation } = productsApi;
שימוש ברכיבים
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { addItem, selectCartCount } from '@/store/slices/cartSlice';
import { useGetProductsQuery } from '@/store/api/productsApi';
function ProductCard({ productId }: { productId: string }) {
const dispatch = useAppDispatch();
const cartCount = useAppSelector(selectCartCount);
const { data: product, isLoading } = useGetProductsQuery({ id: productId });
if (isLoading) return <Skeleton />;
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => dispatch(addItem(product))}>
В корзину ({cartCount})
</button>
</div>
);
} למה לבחור ב-Redux DevTools?
ה-Redux DevTools Extension מאפשר לסקור את כל היסטוריית הפעולות, לנסוע לכל מצב קודם (time-travel), ולייצא/לייבא מצב לשחזור באגים. בתצורה שלמעלה, אנו מאפשרים זאת עם מעקב אחר קריאות פעולה. זה מפחית את זמן מציאת הבאגים ב-30%.
מה כלול בעבודה?
- Store טיפוסי עם slices לכל תחום
- נקודות קצה של RTK Query עם invalidation מלא של cache
- Selectors עם memoization ואופטימיזציית re-render
- אינטגרציה של Redux DevTools עם מעקב
- תיעוד ארכיטקטורת מצב והגירה מקוד legacy
לצוות שלנו יש יותר מ-5 שנות ניסיון ייצור עם Redux בפרויקטים ברמות מורכבות שונות — מחנויות e-commerce ועד לוחות בקרה עם מאות רכיבים. אנו מבטיחים ארכיטקטורה ניתנת להרחבה וצפויה. הזמינו הגדרת Redux לפרויקט שלכם — ננתח את המצב הנוכחי שלכם ונציע תוכנית הגירה. קבלו ייעוץ על אופטימיזציית ניהול מצב — צרו קשר, ונתאים ארכיטקטורה לקנה המידה שלכם.
הערכות לוחות זמנים
| שלב | משך |
|---|---|
| הגדרת Store, slices ו-hooks טיפוסיים | 3-5 ימים |
| נקודות קצה של RTK Query ואינטגרציית רכיבים | 5-8 ימים |
| Selectors עם memoization, אופטימיזציה ובדיקות | 4-6 ימים |
| תיעוד, סקירת קוד ו-DevTools | 2-3 ימים |
פונקציות reducer נבדקות בבידוד ללא React — זהו אחד היתרונות המרכזיים של ארכיטקטורת Redux על פני hooks. פנו אלינו להגדרת Redux — נביא סדר לניהול המצב שלכם.







