Menu

ההוק useReducer ב-React: תחביר, דוגמאות, מול useState

useReducer שומר state ומעביר כל עדכון לפונקציית reducer אחת: שולחים action, וה-reducer מחזיר את ה-state הבא. למדו את התחביר, איך כותבים reducer טהור, דוגמה של רשימת משימות, אתחול עצל, ומתי לבחור בו על פני useState.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

useReducer הוא הוק של React ששומר state כמו useState, אבל מעביר כל דרך לשנות אותו לפונקציה אחת, ה-reducer. הקומפוננטה קוראת ל-dispatch עם אובייקט action כמו { type: 'increment' }, ו-React מעבירה את ה-state הנוכחי ואת ה-action הזה ל-reducer, שמחזיר את ה-state הבא.

הכפתורים לא אומרים איך הספירה משתנה, רק מה קרה. כל החשבון נמצא ב-reducer. הוסיפו case 'double' שמחזיר { count: state.count * 2 } וכפתור ששולח אותו, כדי לראות איך סוג עדכון חדש משתלב.

התחביר

const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional

function reducer(state, action) {
    // return the next state
}
  • reducer היא פונקציה (state, action) => nextState. הגדירו אותה מחוץ לקומפוננטה; היא לא צריכה שום דבר מה-scope של הקומפוננטה.
  • initialArg הוא ה-state ההתחלתי, שמשמש רק ברינדור הראשון.
  • init אופציונלי. אם מעבירים אותו, ה-state ההתחלתי הוא init(initialArg) במקום.
  • state הוא ה-state הנוכחי ברינדור הזה.
  • dispatch(action) שולח action ל-reducer ומתזמן רינדור עם התוצאה. הוא יציב: זו אותה פונקציה בכל רינדור, ולכן בטוח להעביר אותו מטה או לרשום אותו כתלות.

action יכול להיות כל ערך, אבל לפי המוסכמה זה אובייקט עם מחרוזת type שמתארת מה קרה, ועוד כל נתון שה-reducer צריך: { type: 'added', text: 'Buy milk' }.

כתיבת reducer

רוב ה-reducers הם switch על action.type, עם case אחד לכל סוג אירוע. כל case מחזיר state חדש לגמרי; הוא אף פעם לא משנה את הישן. ה-case של default זורק שגיאה, כך שטעות הקלדה כמו dispatch({ type: 'incremnet' }) נכשלת ברעש במקום לא לעשות כלום.

תנו ל-actions שמות לפי מה שהמשתמש עשה (added, toggled, deleted), לא לפי שינוי ה-state שיש לכם בראש (setTodos). כך הקומפוננטה נקראת כמו רשימת אירועים, וה-reducer הוא המקום היחיד שמחליט מה כל אירוע אומר.

reducers חייבים להיות טהורים

reducer הוא פונקציה טהורה: עבור אותו state ואותו action, הוא מחזיר את אותה תוצאה, ולא עושה שום דבר אחר. כלומר אין בו mutation, בקשות, טיימרים, Math.random() או Date.now(). React סומכת על זה. בפיתוח תחת StrictMode, React קוראת ל-reducer שלכם פעמיים לכל action ושומרת תוצאה אחת, כדי לעזור לכם לשים לב ל-reducer שאינו טהור. התצוגה המקדימה כאן רצה כמו build של פרודקשן, ולכן הדוגמאות בדף הזה קוראות לו פעם אחת.

כלל ה-mutation הוא זה שאנשים שוברים הכי הרבה. דחיפה לתוך state.todos והחזרת state מחזירה את אותו אובייקט, ולכן React לא רואה שינוי ומדלגת על הרינדור, אותה מלכודת שמתוארת ב-עדכון מערכים ואובייקטים. בנו מערכים ואובייקטים חדשים עם spread, map ו-filter.

// Wrong: mutates and returns the same object
case 'added':
    state.todos.push(action.todo);
    return state;

// Right: returns a new object with a new array
case 'added':
    return { ...state, todos: [...state.todos, action.todo] };

תופעות לוואי ששייכות לאירוע, כמו שמירה בשרת, הולכות ב-event handler ליד dispatch. תופעות לוואי ששייכות ל-state, כמו סנכרון שלו ל-localStorage, הולכות באפקט.

רשימת משימות עם useReducer

הנה ה-reducer עושה עבודה אמיתית: הוספה, סימון ומחיקה של משימות. ה-console.log בראש ה-reducer מדפיס כל action, דרך נוחה לראות איך ה-state משתנה בזמן דיבוג. זו תופעת הלוואי היחידה שבדרך כלל מקובלת ב-reducer, כי היא לא משנה כלום; הוציאו אותה לפני שמשחררים לפרודקשן.

הוסיפו משימה, סמנו תיבה ומחקו אחת, ואז קראו את הקונסול: כל שינוי הוא שורה אחת שמציינת את ה-action והנתונים שלו. הטקסט של השדה נשאר ב-useState, כי הוא מקומי לשדה ואף אירוע אחר לא נוגע בו. שילוב של שני ההוקים בקומפוננטה אחת הוא דבר רגיל.

ה-id של המשימה החדשה נוצר ב-handler של הלחיצה ועובר בתוך ה-action, כך שה-reducer רק מעתיק אותו. כתיבת id: nextId++ בתוך ה-reducer הייתה הופכת אותו ללא טהור: תחת StrictMode בפיתוח הקריאה השנייה הייתה מדלגת על id.

dispatch לא משנה את ה-state מיד

dispatch עובד כמו setter של useState: הוא מתזמן רינדור, והמשתנה state ב-handler הנוכחי שומר את הערך הישן. כדי להשתמש ב-state החדש מיד, חשבו אותו בעצמכם על ידי קריאה ל-reducer.

לחצו כמה פעמים. שורת הלוג הראשונה תמיד צעד אחד מאחור, והשנייה תואמת למספר על הכפתור אחרי הרינדור. מכיוון שה-reducer הוא פונקציה רגילה, בטוח לקרוא לו בעצמכם, וזה עוד יתרון של שמירה עליו טהור.

useState מול useReducer

שני ההוקים שומרים state, וכל מה שכותבים עם אחד אפשר לכתוב עם השני. ההבדל הוא איפה לוגיקת העדכון חיה.

useStateuseReducer
לוגיקת עדכוןבכל event handlerבפונקציית reducer אחת
מתאים לכמה ערכים עצמאייםכמה שדות שמשתנים יחד בגלל הרבה אירועים
כמות קודפחות ל-state פשוטיותר בהתחלה, פחות כשהאירועים מתרבים
בדיקותבודקים דרך הקומפוננטהבודקים את ה-reducer כפונקציה רגילה
דיבוגמחפשים איזה handler קבע אותורושמים כל action במקום אחד

פנו ל-useReducer כשאתם שמים לב שאותו state מתעדכן בהרבה handlers, כשאירוע אחד צריך לשנות כמה חלקי state שחייבים להישאר עקביים, או כש-handler הוא בעיקר לוגיקה על ה-state הבא. ל-toggle או לשדה טקסט, useState קצר וברור יותר. מעבר מאוחר יותר לא קשה: מחליפים את ה-setters ב-dispatches ומעבירים את הלוגיקה ל-cases.

אתחול עצל

אם כותבים את ה-state ההתחלתי כקריאה לפונקציה, כמו useReducer(reducer, createInitialState('Ada')), הקריאה הזו רצה בכל רינדור, למרות ש-React משתמשת בתוצאה שלה רק בפעם הראשונה. אם בניית ה-state ההתחלתי יקרה, או צריכה להיגזר מ-prop, העבירו ארגומנט שלישי: פונקציית init. React קוראת ל-init(initialArg) פעם אחת, ברינדור הראשון.

הקלידו בשדה: הקומפוננטה מתרנדרת בכל הקשה, אבל createInitialState נרשמת רק פעם אחת. עכשיו שנו את הקריאה ל-useReducer(reducer, createInitialState('Ada')) והקלידו שוב. הפונקציה רצה בכל רינדור, ו-React זורקת את התוצאה בכל פעם אחרי הראשונה.

העבירו את הפונקציה עצמה, לא את התוצאה של הקריאה אליה. useReducer(reducer, 'Ada', createInitialState) הוא עצל; useReducer(reducer, createInitialState('Ada')) לא.

useReducer עם קונטקסט

reducer משתלב טוב עם קונטקסט כשעץ עמוק צריך את ה-state. שימו את ה-state ואת dispatch בקונטקסט למעלה, וכל קומפוננטה מתחת יכולה לקרוא את ה-state או לשלוח actions בלי props שעוברים דרך כל שכבה. מכיוון ש-dispatch אף פעם לא משתנה, קומפוננטות שרק שולחות actions יכולות לקרוא קונטקסט dispatch נפרד ולדלג על רינדור מחדש כשה-state משתנה.

import { createContext, useContext, useReducer } from 'react';

const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);

export function TodosProvider({ children }) {
    const [todos, dispatch] = useReducer(todosReducer, []);
    return (
        <TodosContext value={todos}>
            <TodosDispatchContext value={dispatch}>{children}</TodosDispatchContext>
        </TodosContext>
    );
}

function AddTodo() {
    const dispatch = useContext(TodosDispatchContext);
    return <button onClick={() => dispatch({ type: 'added', text: 'New' })}>Add</button>;
}

ב-React 19 אובייקט קונטקסט עובד כ-provider של עצמו (<TodosContext value={...}>); גם <TodosContext.Provider> עדיין עובד. הדף על useContext מכסה איך קונטקסט מגיע לקומפוננטות ומתי הוא מרנדר אותן מחדש.

שאלות נפוצות

מה זה useReducer ב-React?

הוק ל-state שהעדכונים שלו מתוארים כ-actions. קוראים ל-const [state, dispatch] = useReducer(reducer, initialState), ואז ל-dispatch({ type: 'added' }) מתוך event handlers. React מעבירה את ה-state הנוכחי ואת ה-action ל-reducer שלכם, ומה שהוא מחזיר הופך ל-state הבא.

מתי להשתמש ב-useReducer במקום ב-useState?

כשכמה אירועים מעדכנים את אותו state בדרכים קשורות, כשה-state הבא תלוי בכמה שדות בבת אחת, או כשלוגיקת העדכון ארוכה מספיק כדי שתרצו אותה בפונקציה אחת שאפשר לבדוק, מחוץ לקומפוננטה. לערך אחד או שניים עצמאיים, useState פשוט יותר.

למה reducer חייב להיות טהור?

React עשויה לקרוא ל-reducer שלכם יותר מפעם אחת עבור אותו action (StrictMode עושה את זה בכוונה בפיתוח) ומצפה לאותה תוצאה בכל פעם. לכן reducer לא יכול לשנות את ה-state, לטעון נתונים, לקבוע טיימרים או לקרוא ערכים אקראיים. עשו את הדברים האלה ב-event handlers או באפקטים.

האם dispatch מעדכן את ה-state מיד?

לא. כמו setter של useState, dispatch מתזמן רינדור. בתוך ה-event handler הנוכחי state עדיין מחזיק את הערך הישן; ה-state החדש מופיע ברינדור הבא.

מה הארגומנט השלישי של useReducer?

פונקציית init אופציונלית. כשמעבירים אותה, React מחשבת את ה-state ההתחלתי בתור init(initialArg), ורק ברינדור הראשון. זה שימושי כשבניית ה-state ההתחלתי יקרה או תלויה ב-prop.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל