במאמר זה אנו נדון מה זה Redux, למה אנחנו צריכים אותה ומתי משתמשים בה. לאחר מכן נראה כיצד ליישם Redux Toolkit (הגרסה החדשה) עם TypeScript ב-React.js.
מה זה Redux?
Redux היא ספריית JavaScript פופולרית שמשמשת לניהול מצב / מידע באפליקציה בצורה מסודרת ומובנת (state management). לדוגמה, באתרי E-commerce משתמשים ב-Redux כדי לשמור ולנהל מגוון פעולות שאנחנו עושים באתרים מסוג זה, לדוגמה ניהול המוצרים שהוספנו לסל, ניהול כמות המוצרים שיש לנו בסל, ניהול השילוח וכדומה.
הטרמינולוגיה של Redux:
Redux מורכבת משלושה יחידות שפועלות ביחד על מנת ליצור זרימה וניהול תקין של אותן הנתונים / מצב:
Store:
store היא אובייקט המחזיק את כל הנתונים ומצב האפליקציה. ה-store הוא ה-single source of truth. מכאן נשאב את הנתונים שאספנו מהאתר ונזרים אותם לשאר חלקי האפליקציה שצריכים את אותו מידע.
By Mosh Hamedani
Actions:
actions הם אובייקט שמייצגים את הכוונה לשנות את מצב האפליקציה. לכל action יש מאפיין 'type' המתאר את הפעולה שהוא ירצה לבצע ומאפיין(אופציונלי) 'payload' המכיל נתונים נוספים הדרושים לביצוע הפעולה. בגרסה החדשה של Redux שאותה נלמד היום(Redux Toolkit) לא חובה לתת type, המאפיין type יהיה לפי שם ה-slice's name (שעליו נלמד) + השם של הפונקציה: counter/incrementByAmount.
reducers הם פונקציות שלוקחות את המצב הנוכחי של האפליקציה (state) ו-action(אופציונלי) כארגומנטים ומחזירות מצב חדש(מעודכן) על סמך הלוגיקה של ה-Reducer Function. בגרסת ה-Redux Toolkit, יש שם שחקן חדש שנכנס לתמונה שמאגד את כל ה-actions ופונקציות ה-reducers והוא נקרא createSlice. לכל Slice יש מספר מאפיינים שעוזרים לנו לנהל את המידע ולבצע מניפולציות על המידע בצורה מסודרת יותר:
מה בדרך כלל createSlice מכיל:
name - שם ה-slice ומשמש ליצירת type לכל action באופן אוטומטי.
initialState - המידע הראשוני שצריך להיות באותו reducer, איזשהו מידע שחייב להיות לו כבר ברגע הראשון. לדוגמה, אם נרצה על כל לחיצה להגיל ב-1 את המספר, זה אומר שבהתחלה אמור להיות ערך התחלתי כמו 0:
reducers - אובייקט שמכיל את ה-action וגם את ה- reducer functions. הם בעצם key value pair; ה-key זה שם ה-actions וה-value זה ה-reducer functions.
extraReducers - שבו אתם יכולים להגדיר (לא חובה) actions ו-reducer functions שלא הוגדרו ב-reducers אובייקט אלא הוגדרו ב-slice אחר או מחוץ לאובייקט createSlice.
לפני שנראה את התמונה של כולם ביחד, אנחנו רוצים לדבר אתכם על האפשרות של לבצע קריאות API עם Redux Toolkit.
createAsyncThunk:
באפשרותנו להשתמש (לא חובה) בפונקציה createAsyncThunk שמאפשרת טיפול פשוט יותר בבקשות לשרת וטיפול בשגיאות, ומשפר את ארגון הקוד. פונקציה זו באופן אוטומטי נותנת לכם גישה לשלושת המצבים בהם יכולה הבקשה שלכם להיות (pending, fulfilled and rejected).
יישום Redux Toolkit עם TypeScript:
לפני שאתם רואים את התמונות, אנחנו ממליצים לקורא את המאמר הזה לפחות פעמיים כדי להבין את Redux & Redux Toolkit. זה מנגנון שיכול להיות מסובך להבנה בפעם הראשונה, לכן קריאה שנייה בהחלט תתרום לכם. על כל שורת קוד, הוספנו הערה שתעזור לכם להבין טוב יותר את הקוד שלנו.
שלב ראשון - הורדת ה-packages:
npm i @reduxjs/toolkit
npm i react-redux
שלב שני - יצירת תיקיות וקבצים לניהול תקין של Redux Toolkit:
app\hooks.ts
app\store.ts
features\counter\slice.ts
features\counter\api.ts
index.tsx
features\counter\Counter.tsx
בקובץ הראשון: hooks.ts
// Import hooks, types from react-redux and app store. import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import type { RootState, AppDispatch } from './store';
// Custom useDispatch hook with AppDispatch type for type safety export const useAppDispatch = () => useDispatch<AppDispatch>();
// Custom useSelector hook with RootState type for type safety export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
בקובץ השני: store.ts
// Import required functions and slices import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice';
// Configure Redux store with counter reducer export const store = configureStore({ reducer: { counter: counterReducer, }, });
// Define and export AppDispatch and RootState types export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType<typeof store.getState>;
בקובץ השלישי: slice.ts
// Import necessary functions and types from Redux Toolkit import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { RootState } from '../../app/store'; import { fetchCount } from './counterAPI';
// Initialize counter slice with state, reducers, extraReducers. export const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => { state.value += 1; // Increment the counter by 1 }, decrement: (state) => { state.value -= 1; // Decrement the counter by 1 }, incrementByAmount: (state, action: PayloadAction<number>) => { // Increment the counter by the specified amount state.value += action.payload; }, }, extraReducers: (builder) => { builder .addCase(incrementAsync.pending, (state) => { // Status 'loading' during pending async request. state.status = 'loading'; }) .addCase(incrementAsync.fulfilled, (state, action) => { // Status 'idle' during fulfilled async request. state.status = 'idle'; // Increment the counter by the async response value state.value += action.payload; }) .addCase(incrementAsync.rejected, (state) => { // Status 'failed' during rejected async request. state.status = 'failed'; }); }, });
// Export actions for use elsewhere in the app. export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// Create selector for current count value from state. export const selectCount = (state: RootState) => state.counter.value;
// Export the reducer for use in the store export default counterSlice.reducer;
בקובץ הרביעי: api.ts
// Mock async data request function export function fetchCount(amount = 1) { return new Promise<{ data: number }>((resolve) => setTimeout(() => resolve({ data: amount }), 1000) ); }
בקובץ החמישי: index.tsx
import React from 'react'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { store } from './app/store'; import App from './App'; import reportWebVitals from './reportWebVitals'; import './index.css';
useAppSelector - נועד כדי למשוך פיסת מידע מה-store שלנו.
useAppDispatch - מחזיר פונקציה (dispatch) שנועדה לבצע קריאה ל-actions שהגדרנו.
סיכום מערכת Redux:
כאשר אנו רוצים להשתמש בנתונים מסוימים מה-Store, נשתמש ב-hook בשם useAppSelector, ונקבע בתוכו את ה-selectorState הרלוונטי, במקרה שלנו – selectCount.
על מנת לעדכן את הנתונים הקיימים ב-Store, נשתמש ב-hook בשם useAppDispatch, שמחזיר פונקציה בשם dispatch.
כפי שניתן לראות בשורות 34, 44, 60 ו-67, אנו משתמשים ב-dispatch ומעבירים לה את ה-action המתאים, הכולל שני תכונות – 'type' ו-'payload'.
כל action כולל את תכונת 'type' המתארת את הפעולה שיש לבצע(אם לא מגדירים type, יש type ברירת מחדל), ותכונה (אופציונלית) בשם 'payload', המכילה נתונים נוספים הדרושים לביצוע הפעולה.
לאחר שקראנו ל-action המתאים, התהליך ממשיך ל-reducer, שבוחר את ה-action הרלוונטי ומבצע את העדכון המבוקש.
כשהעדכון הושלם, הנתונים המעודכנים מגיעים לכל קומפוננטה המשתמשת באותם הנתונים, ולאחר מכן מתבצע רינדור (ריענון) של הקומפוננטות המעורבות.
Context API VS Redux
לפעמים עולה שיח למה אנחנו צריכים להשתמש ב-Redux Toolkit כשיש לנו Context API. בואו ננסה לענות על השאלה הזו בקצרה:
Context API פשוט וקל יותר לשימוש והקמה, בעוד Redux דורש יותר קוד ועקומת למידה תלולה יותר.
Context API מובנה בתוך React, בעוד ש-Redux היא ספרייה נפרדת שצריך להתקין ולהגדיר.
Context API מתאים יותר לאפליקציות קטנות עד בינוניים, בעוד ש-Redux מיועד לאפליקציות גדולות ומורכבות עם יותר זרימה של נתונים.
ל-Redux יש מספר אופטימיזציות מובנות של ביצועים כדי למזער רינדורים מיותרים, בעוד של-Context API אין אופטימיזציות כאלו.