useReducer הוא React Hook המאפשר לך להוסיף reducer לרכיב שלך.
const [state, dispatch] = useReducer(reducer, initialArg, init?)הפניה
useReducer(reducer, initialArg, init?)
התקשר ל-useReducer ברמה העליונה של הרכיב שלך כדי לנהל את ה-state שלו עם מפחית.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...פרמטרים
reducer: פונקציית המפחית שמציינת כיצד ה-state מתעדכן. זה חייב להיות טהור, צריך לקחת את state ואת הפעולה כטיעונים, ועליו להחזיר את ה-state הבא. מצב ופעולה יכולים להיות מכל סוג.initialArg: הערך שממנו מחושב ה-state הראשוני. זה יכול להיות ערך מכל סוג שהוא. אופן החישוב של ה-state ההתחלתי ממנו תלוי בארגומנטinitהבא.- אופציונלי
init: פונקציית האתחול שאמורה להחזיר את ה-state הראשוני. אם זה לא צוין, ה-state הראשוני מוגדר ל-initialArg. אחרת, ה-state הראשוני מוגדר לתוצאה של קריאה ל-init(initialArg).
מחזירה
useReducer מחזיר מערך עם שני ערכים בדיוק:
- ה-state הנוכחי. במהלך העיבוד הראשון, הוא מוגדר ל-
init(initialArg)אוinitialArg(אם איןinit). - הפונקציה
dispatchהמאפשרת לך לעדכן את ה-state לערך אחר ולהפעיל עיבוד מחדש.
אזהרות
useReducerהוא Hook, אז אתה יכול לקרוא לו רק ברמה העליונה של הרכיב שלך או Hooks משלך. אתה לא יכול לקרוא לזה בתוך לולאות או תנאים. אם אתה צריך את זה, חלץ רכיב חדש והעביר את ה-state לתוכו.- במצב קפדני, React יתקשר למפחית ולאתחול שלך פעמיים כדי לעזור לך למצוא זיהומים מקריים. זוהי התנהגות לפיתוח בלבד ואינה משפיעה על הייצור. אם המפחית והמאתחל שלך טהורים (כפי שהם צריכים להיות), זה לא אמור להשפיע על ההיגיון שלך. התוצאה מאחת השיחות מתעלמת.
dispatch פונקציה
הפונקציה dispatch המוחזרת על ידי useReducer מאפשרת לך לעדכן את ה-state לערך אחר ולהפעיל עיבוד מחדש. עליך להעביר את הפעולה כארגומנט היחיד לפונקציה dispatch:
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...React יגדיר את ה-state הבא לתוצאה של קריאה לפונקציה reducer שסיפקת עם ה-state הנוכחי והפעולה שהעברת ל-dispatch.
פרמטרים
action: הפעולה שבוצעה על ידי ה-user. זה יכול להיות ערך מכל סוג שהוא. לפי המוסכמה, פעולה היא בדרך כלל אובייקט עם מאפייןtypeהמזהה אותו, ובאופן אופציונלי, מאפיינים אחרים עם מידע נוסף.
מחזירה
לפונקציות dispatch אין ערך החזרה.
אזהרות
-
הפונקציה
dispatchמעדכנת רק את המשתנה state עבור העיבוד הבא. אם תקרא את המשתנה state לאחר קריאה לפונקציהdispatch, אתה עדיין תקבל את הערך הישן שהיה על המסך לפני השיחה שלך. -
אם הערך החדש שאתה מספק זהה ל-
stateהנוכחי, כפי שנקבע על ידיObject.isהשוואה, React ידלג על עיבוד מחדש של הרכיב והילדים שלו. זוהי אופטימיזציה. ייתכן שReact עדיין צריך לקרוא לרכיב שלך, אבל זה אמור להשפיע על התוצאה. -
React אצות state עדכונים. הוא מעדכן את המסך לאחר שכל מטפלי האירועים פעלו וקראו לפונקציות
setשלהם. זה מונע רינדורים חוזרים מרובים במהלך אירוע בודד. במקרה הנדיר שאתה צריך לאלץ את React לעדכן את המסך מוקדם יותר, למשל כדי לגשת ל-DOM, אתה יכול useflushSync.
שימוש
הוספת מפחית לרכיב
התקשר ל-useReducer ברמה העליונה של הרכיב שלך כדי לנהל את state עם מפחית.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...useReducer מחזיר מערך עם שני פריטים בדיוק:
- הנוכחי state של משתנה state זה, מוגדר תחילה ל-state הראשוני שסיפקת.
- הפונקציה
dispatchהמאפשרת לך לשנות אותה בתגובה לאינטראקציה.
כדי לעדכן את מה שמופיע על המסך, קרא ל-dispatch עם אובייקט המייצג את מה שה-user עשה, הנקרא action:
function handleClick() {
dispatch({ type: 'incremented_age' });
}React יעביר את ה-state הנוכחי ואת הפעולה לפונקציית המפחית שלך. המפחית שלך יחשב ויחזיר את ה-state הבא. React יאחסן את ה-state הבא, יעבד איתו את הרכיב שלך ויעדכן את ממשק המשתמש.
import { useReducer } from 'react'; function reducer(state, action) { if (action.type === 'incremented_age') { return { age: state.age + 1 }; } throw Error('Unknown action.'); } export default function Counter() { const [state, dispatch] = useReducer(reducer, { age: 42 }); return ( <> <button onClick={() => { dispatch({ type: 'incremented_age' }) }}> Increment age </button> <p>Hello! You are {state.age}.</p> </> ); }
useReducer דומה מאוד ל-useState, אבל הוא מאפשר לך להעביר את לוגיקת העדכון state ממטפלי אירועים לפונקציה יחידה מחוץ לרכיב שלך. קרא עוד על בחירה בין useState ל-useReducer.
כתיבת פונקציית המפחית
פונקציית מפחית מוצהרת כך:
function reducer(state, action) {
// ...
}לאחר מכן עליך למלא את הקוד שיחשב ויחזיר את ה-state הבא. לפי המוסכמה, מקובל לכתוב אותו כ-switch statement. עבור כל case ב-switch, חשב והחזר כמה state הבא.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}לפעולות יכולות להיות כל צורה. לפי המוסכמה, מקובל להעביר אובייקטים עם מאפיין type המזהה את הפעולה. זה צריך לכלול את המידע המינימלי הדרוש שהמפחית צריך כדי לחשב את ה-state הבא.
function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...שמות סוגי הפעולה הם מקומיים לרכיב שלך. כל פעולה מתארת אינטראקציה בודדת, גם אם זה מוביל לשינויים מרובים בנתונים. הצורה של state היא שרירותית, אבל בדרך כלל זה יהיה אובייקט או מערך.
קרא את חילוץ state היגיון למפחית כדי ללמוד עוד.
Example 1 of 3: טופס (אובייקט)
בדוגמה זו, המפחית מנהל אובייקט state עם שני שדות: name ו-age.
import { useReducer } from 'react'; function reducer(state, action) { switch (action.type) { case 'incremented_age': { return { name: state.name, age: state.age + 1 }; } case 'changed_name': { return { name: action.nextName, age: state.age }; } } throw Error('Unknown action: ' + action.type); } const initialState = { name: 'Taylor', age: 42 }; export default function Form() { const [state, dispatch] = useReducer(reducer, initialState); function handleButtonClick() { dispatch({ type: 'incremented_age' }); } function handleInputChange(e) { dispatch({ type: 'changed_name', nextName: e.target.value }); } return ( <> <input value={state.name} onChange={handleInputChange} /> <button onClick={handleButtonClick}> Increment age </button> <p>Hello, {state.name}. You are {state.age}.</p> </> ); }
הימנעות מיצירה מחדש של ה-state הראשוני
React שומר את ה-state הראשוני פעם אחת ומתעלם ממנו בעיבודים הבאים.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...למרות שהתוצאה של createInitialState(username) היא רק used עבור העיבוד הראשוני, אתה עדיין קורא לפונקציה הזו בכל עיבוד. זה יכול להיות בזבזני אם זה יוצר מערכים גדולים או ביצוע חישובים יקרים.
כדי לפתור זאת, אתה יכול להעביר אותה כפונקציית initializer ל-useReducer בתור הארגומנט השלישי במקום זאת:
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...שימו לב שאתם מעבירים את createInitialState, שהיא הפונקציה עצמה, ולא את createInitialState(), שהיא התוצאה של הקריאה לה. בדרך זו, ה-state הראשוני לא נוצר מחדש לאחר האתחול.
בדוגמה שלמעלה, createInitialState לוקח ארגומנט username. אם המאתחל שלך אינו זקוק למידע כלשהו כדי לחשב את ה-state הראשוני, תוכל להעביר את null כארגומנט השני ל-useReducer.
Example 1 of 2: העברת פונקציית האתחול
דוגמה זו מעבירה את פונקציית האתחול, כך שהפונקציה createInitialState פועלת רק במהלך האתחול. זה לא פועל כאשר רכיב מעבד מחדש, כגון כאשר אתה מקליד בקלט.
import { useReducer } from 'react'; function createInitialState(username) { const initialTodos = []; for (let i = 0; i < 50; i++) { initialTodos.push({ id: i, text: username + "'s task #" + (i + 1) }); } return { draft: '', todos: initialTodos, }; } function reducer(state, action) { switch (action.type) { case 'changed_draft': { return { draft: action.nextDraft, todos: state.todos, }; }; case 'added_todo': { return { draft: '', todos: [{ id: state.todos.length, text: state.draft }, ...state.todos] } } } throw Error('Unknown action: ' + action.type); } export default function TodoList({ username }) { const [state, dispatch] = useReducer( reducer, username, createInitialState ); return ( <> <input value={state.draft} onChange={e => { dispatch({ type: 'changed_draft', nextDraft: e.target.value }) }} /> <button onClick={() => { dispatch({ type: 'added_todo' }); }}>Add</button> <ul> {state.todos.map(item => ( <li key={item.id}> {item.text} </li> ))} </ul> </> ); }
פתרון בעיות
שלחתי פעולה, אבל רישום מעניק לי את הערך הישן state
קריאה לפונקציה dispatch לא משנה את state בקוד הפועל:
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // Request a re-render with 43
console.log(state.age); // Still 42!
setTimeout(() => {
console.log(state.age); // Also 42!
}, 5000);
}זה בגלל use states מתנהגים כמו תמונת מצב. עדכון state מבקש עיבוד נוסף עם הערך החדש state, אך אינו משפיע על המשתנה state JavaScript הרץ שלך במשתנה המטפל שלך.
אם אתה צריך לנחש את הערך הבא של state, אתה יכול לחשב אותו באופן ידני על ידי קריאה למפחית בעצמך:
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }שלחתי פעולה, אבל המסך לא מתעדכן
React יתעלם מהעדכון שלך אם ה-state הבא שווה ל-state הקודם, כפי שנקבע על ידי השוואה Object.is. זה קורה בדרך כלל כאשר אתה משנה אובייקט או מערך ב-state ישירות:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Wrong: mutating existing object
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Wrong: mutating existing object
state.name = action.nextName;
return state;
}
// ...
}
}שיניתם אובייקט state קיים והחזרתם אותו, אז React התעלם מהעדכון. כדי לתקן זאת, עליך לוודא שאתה תמיד מעדכן אובייקטים ב-state ו-מעדכן מערכים ב-state במקום לשנות אותם:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Correct: creating a new object
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Correct: creating a new object
return {
...state,
name: action.nextName
};
}
// ...
}
}חלק מהמפחית שלי state הופך ללא מוגדר לאחר שליחת
ודא שכל סניף case מעתיק את כל השדות הקיימים בעת החזרת ה-state החדש:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Don't forget this!
age: state.age + 1
};
}
// ...ללא ...state לעיל, ה-state הבא המוחזר יכיל רק את השדה age ולא שום דבר אחר.
כל המפחית שלי state הופך ללא מוגדר לאחר שליחת
אם ה-state שלך הופך באופן בלתי צפוי ל-undefined, סביר להניח שאתה שוכח לכתוב return state באחד המקרים, או שסוג הפעולה שלך אינו תואם לאף אחד מה-case state. כדי למצוא למה, זרוק שגיאה מחוץ ל-switch:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}אתה יכול גם use בודק סוג סטטי כמו TypeScript כדי לתפוס טעויות כאלה.
אני מקבל שגיאה: “יותר מדי עיבודים חוזרים”
אתה עשוי לקבל שגיאה האומרת: Too many re-renders. React limits the number of renders to prevent an infinite loop. בדרך כלל, זה אומר שאתה שולח פעולה ללא תנאי במהלך העיבוד, אז הרכיב שלך נכנס ללולאה: render, dispatch (שמייצג rendering), render, dispatch (שמאפשר עיבוד) וכן הלאה. לעתים קרובות מאוד, זהו caused בטעות בציון מטפל באירוע:
// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>
// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>
// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>אם אינך מוצא את ה-cause של שגיאה זו, לחץ על החץ שליד השגיאה במסוף ועיין בערימת JavaScript כדי למצוא את קריאת הפונקציה הספציפית dispatch האחראית לשגיאה.
פונקציית המפחית או האתחול שלי פועלת פעמיים
ב-מצב קפדני, React יקרא לפונקציות המפחית והמאתחל שלך פעמיים. זה לא אמור לשבור את הקוד שלך.
התנהגות זו של פיתוח בלבד עוזרת לך לשמור על רכיבים טהורים. React uses התוצאה של אחת הקריאות, ומתעלמת מהתוצאה של השיחה השנייה. כל עוד פונקציות הרכיב, האתחול והמפחית שלך טהורות, זה לא אמור להשפיע על ההיגיון שלך. עם זאת, אם הם בטעות טמאים, זה עוזר לך לשים לב לטעויות.
לדוגמה, פונקציית מפחית לא טהורה זו משנה מערך ב-state:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Mistake: mutating state
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}Because React קורא לפונקציית המפחית שלך פעמיים, אתה תראה שהמטלה נוספה פעמיים, כדי שתדע שיש טעות. בדוגמה זו, תוכל לתקן את הטעות על ידי החלפת המערך במקום לשנות אותו:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Correct: replacing with new state
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}כעת, כשפונקציית המפחית הזו טהורה, לקרוא לזה זמן נוסף לא משנה את ההתנהגות. זו הסיבה שReact קורא לזה פעמיים עוזר לך למצוא טעויות. רק פונקציות הרכיב, האתחול והמפחית צריכות להיות טהורות. מטפלי אירועים אינם צריכים להיות טהורים, לכן React לעולם לא יתקשר למטפלי האירועים שלך פעמיים.
קרא את שמירה על רכיבים טהורים למידע נוסף.