חילוץ לוגיקת state ל-reducer

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

You will learn

  • מהי פונקציית reducer
  • איך לשנות את ‘useState’ ל-‘useReducer’
  • מתי להשתמש בreducer
  • איך לכתוב אחד טוב

איחוד היגיון מצב עם reducer

כמה שהרכיבים שלך גדלים במורכבות, זה יהיה קשה יותר לראות במבט אחד את כל הדרכים השונות שבהן מצב הרכיב מתעדכן. לדוגמה, הרכיב ‘TaskApp’ למטה מחזיק מערך של ‘משימות’ בstate ומשתמש בשלושה מטפלי אירועים כדי להוסיף ולערוך משימות:

import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([
      ...tasks,
      {
        id: nextId++,
        text: text,
        done: false,
      },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) {
          return task;
        } else {
          return t;
        }
      })
    );
  }

  function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.id !== taskId));
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

כל אחד מעובדי העסקים שלו קורא ‘שואל’ על מנת לעדכן את הstate. ככל שחשוב זה גדל, כך גדלה כמות ההיגיון של הstate המפוזרת בו. כדי להפחית את המורכבות הזו ולשמור את כל ההיגיון שלך במקום אחד קל לגעת, אתה יכול להעביר את הstate הזה לפונקציה יחידה מחוץ לרכיב שלך, המכונה “reducer”.

reducer הם דרך אחרת בstate. אתה יכול לעבור מ’useState’ ל’useReducer’ בשלושה שלבים:

  1. עבור מstate הגדרה לפעולות שיגור.
  2. כתוב פונקציית reducer.
  3. השתמש בreducer מהרכיב שלך.

שלב 1: מעבר מstate לפעולות שיגור

מטפלי האירועים שלך מציינים כעת מה לעשות על ידי הגדרת מצב:

function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}

function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}

function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}

הסר את כל הלוגיקה של הגדרות הstate. מה שנותר לך הם שלושה מטפלי אירועים:

  • handleAddTask(text) נקרא כאשר המשתמש לוחץ על “הוסף”.
  • handleChangeTask(task) כאשר המשתמש מחליף משימה או לוחץ על “שמור”.
  • handleDeleteTask(taskId) נקרא כאשר משתמש לוחץ על “מחק”.

ניהול מצב עם reducer שונה במקצת מstate ישיר. במקום להגיד ל-React “מה לעשות” על ידי הגדרת מצב, אתה מציין “מה משתמש בדיוק עשה” על ידי שליחת “פעולות” מרופאי פעולות שלך. (לוגיקת עדכון הstate תתקיים במקום אחר!) אז במקום “להגדיר משימות” באמצעות מטפל באירועים, אתה שולח פעולת “הוסף/שונה/מחק משימה”. זה מתאר יותר את הכוונת המשתמש.

function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}

function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}

function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}

האובייקט שאתה מעביר ל-‘dispatch’ נקרא “פעולה”:

function handleDeleteTask(taskId) {
dispatch(
// "action" object:
{
type: 'deleted',
id: taskId,
}
);
}

זהו אובייקט JavaScript רגיל. אתה מחליט מה לשים בו, אבל בדרך כלל הוא צריך להכיל את המינימלי על מה שקרה. (תוסיף את פונקציית ה’שליחות’ עצמה מאוחרת יותר).

Note

לאובייקט פעולה יכול להיות כל צורה.

לפי המוסכמה, מקובל לתת לו ‘סוג’ מחרוזת שמתארת ​​את מה שקרה, להעביר כל מידע נוסף בשדות אחרות. ה-סוג ספציפי לרכיב, אז בדוגמה או 'added' או 'added_task' יהיו בסדר. בחר שם שאומר מה קרה!

dispatch({
// specific to component
type: 'what_happened',
// other fields go here
});

שלב 2: כתוב פונקציית reducer

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

function yourReducer(state, action) {
// return next state for React to set
}

תגיב תגדיר את הstate למה שאתה מחזיר מפחיד.

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

  1. הכריז על הstate הנוכחי (משימות) כארגומנט הראשון.
  2. הכריז על אובייקט פעולה כארגומנט השני.
  3. החזר את הstate next מפחיד (אשר תגיב יגדיר את הstate אליו).

הנה כל הלוגיקה של הגדרות הstate שעברה לפונקציית reducer:

function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
} else if (action.type === 'changed') {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter((t) => t.id !== action.id);
} else {
throw Error('Unknown action: ' + action.type);
}
}

מכיוון שפונקציית הreducer לוקחת מצב (משימות) כארגומנט, אתה יכול להכריז עליו מחוץ לרכיב שלך. זה מקטין את רמת ההזחה ויכול להקל על הקריאה של הקוד שלך.

Note

הקוד שלמעלה משתמש בהצהרות if/else, אבל זה מוסכמה להשתמש בswitch statements בתוך reducer. התוצאה היא, אבל יכול להיות קל יותר לקרוא את הצהרות מתג במבט חטוף.

אנו נשתמש בהם לאורך שאר התיעוד הזה כך:

function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}

אנו ממליצים לעטוף כל בלוק מקרה בסוגריים מתולתלים { ו} כך שמשתנים המוצהרים בתוך מקרה שונים לא יתנגשו זה בזה. כמו כן, מקרה אמור להסתיים בדרך כלל בהחזרה. אם תשכחו להחזיר, הקוד “יפול” למקרה הבא, מה שעלול להוביל לטעויות!

אם אתה עדיין לא מרגיש בנוח עם הצהרות החלף, השימוש ב-if/else הוא לגמרי בסדר.

Deep Dive

מדוע קוראים לreducers כך?

למרות שreducers יכולים “להפחית” את כמות הקוד בתוך הרכיב שלך, הם למעשה נקראים על שם פעולת reduce() יכולים לבצע על מערכים.

פעולת reduce() מאפשרת לך לקחת מערך ו”לצבור” ערך בודד מתוך רבים:

const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5

הפונקציה שאתה מעביר ל-‘reduce’ ידועה בתור “reducer”. הוא לוקח את התוצאה עד כה ואת הפריט הנוכחי, ואז הוא מחזיר את התוצאה הבאה. reducerי תגובה הם דוגמה לא רעיון: הם לוקחים את state עד כה ו_פעולה_, ומחזירים את state הבא. בדרך זו, הם צוברים לאורך זמן הstate.

אתה יכול אפילו להשתמש בשיטת reduce() עם initialState ומערך של פעולות כדי לחשב את הstate הסופי על ידי העברת פונקציית הreducerה שלך אליו:

import tasksReducer from './tasksReducer.js';

let initialState = [];
let actions = [
  {type: 'added', id: 1, text: 'Visit Kafka Museum'},
  {type: 'added', id: 2, text: 'Watch a puppet show'},
  {type: 'deleted', id: 1},
  {type: 'added', id: 3, text: 'Lennon Wall pic'},
];

let finalState = actions.reduce(tasksReducer, initialState);

const output = document.getElementById('output');
output.textContent = JSON.stringify(finalState, null, 2);

כנראה שלא תצטרכו לעשות זאת בעצמכם, אבל זה דומה למה ש-React עושה!

שלב 3: השתמש בreducer מהרכיב שלך

לבסוף, עליך לחבר את ‘משימות reducerות’ לרכיב שלך. ייבא את ה- ‘useReducer’ מ-React:

import { useReducer } from 'react';

אז אתה יכול להחליף את ‘useState’:

const [tasks, setTasks] = useState(initialTasks);

עם ‘useReducer’ כך:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

ה- useReducer Hook דומה ל-useState - עליך להעביר לו מצב התחלתי והוא מחזיר ערך stateful ודרך להגדיר מצב (במקרה זה, פונקציית השילוח). אבל זה קצת שונה.

ה- ‘useReducer’ Hook לוקח שני ארגומנטים:

  1. פונקציית reducer
  2. מצב ראשוני

וזה מחזיר:

  1. ערך ממלכתי
  2. פונקציית שיגור (כדי “לשלוח” פעולות משתמש לreducer)

עכשיו זה מחובר לגמרי! כאן, הreducer מוצהר בתחתית קובץ הרכיבים:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

אם תרצה, אתה יכול אפילו להעביר את הreducer לקובץ אחר:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import tasksReducer from './tasksReducer.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

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

השוואת useState ו-useReducer

לreducers אין חסרונות! הנה כמה דרכים שבהן תוכל להשוות ביניהם:

  • קוד גודל: בדרך כלל, עם ‘useState’ אתה צריך לכתוב פחות קוד מראש. עם ‘useReducer’, אתה צריך לכתוב גם פונקציית reducers ו פעולות שיגור. עם זאת, ‘useReducer’ יכול לעזור לצמצם את הקוד אם מטפלים אירועים רבים משנים מצב בצורה דומה.
  • קריאה: ‘useState’ קל מאוד לקריאה כאשר עדכוני הstate פשוטים. כשהם נעשים מורכבים יותר, הם יכולים למלא את הקוד של הרכיב והקשות על הסריקה. במקרה זה, ‘useReducer’ מאפשר לך להפריד בצורה נקייה את איך של לוגיקת העדכון מה קרה של מטפלי אירועים.
  • ניפוי באגים: כאשר יש לך באג עם ‘useState’, זה יכול להיות קשה לדעת היכן הstate הוא גדר בצורה שגויה, ו-למה. עם useReducer, אתה יכול להוסיף יומן מסוף לreducer שלך כדי לראות את כל עדכון מצב, ו-למה זה קרה (בשל איזו פעולה). אם כל פעולה נכונה, תדע שהטעות היא בלוגית הreducerה עצמה. עם זאת, אתה צריך לעבור יותר קוד מאשר עם ‘useState’.
  • בדיקה: reducer היא פונקציה טהורה שאינה תלויה ברכיב שלך. זה אומר שאתה יכול לייצא ולבדוק אותו בנפרד בבידוד. למרות שבדרך כלל עדיף לבדוק רכיבים בסביבה מציאותית יותר, עבור לוגיקה של עדכון מצב מורכב זה יכול להיות שימושי לטעון שreducer שלך מחזיר מצב מסוים עבור מצב ופעולה ראשוניים מסוימים.
  • העדפה אישית: יש אנשים שאוהבים reducer, אחרים לא. זה בסדר. זה עניין של העדפה. אתה תמיד יכול להמיר בין ‘useState’ ו-‘useReducer’ הלוך ושוב: הם שווים!

אנו ממלי להשתמש בreducer אם אתה נתקל בקשר בבאגים עקב עדכוני מצב שגויים ברכיבים, וברצונך להכניס מבנה נוסף לקוד שלו. לא חייבים להשתמש במקסימום לכל דבר: אתם מחפשים לערבב ולהתאים! אתה יכול אפילו ‘useState’ ו-‘useReducer’ באותו רכיב.

reducerי כתיבה היטב

זכור את שני הטיפים הבאים בעת כתיבת reducer:

  • reducer חייבים להיות טהורים. בדומה לעדכון מצב פונקציות, reducer פועלים על העיבוד! (פעולות עומדות לפי עד לעיבוד הבא.) משמעות הדבר היא שreducers חייבים להיות טהורים - אותן כניסות תמיד מביאות לאותו פלט. אסור להם לשלוח בקשות, לתזמן פסקי זמן או לבצע פעולה לוואי כלשהן (פעולות שמשפיעות על דברים מחוץ לרכיב). עליהם לעדכן את objects ו-מערכים ללא מוטציות.
  • כל פעולה מתארת ​​אינטראקציה של משתמש בודד, גם אם זה מוביל לשינויים מרובים בנתונים. לדוגמה, אם לוחץ משתמש על “איפוס” בטופס עם חמש שדות המנוהל על ידי reducer, הגיוני יותר לשלוח פעולת reset_form אחת ולא חמש פעולות set_field נפרדות. אם אתה רושם כל פעולה בreducer, יומן זה אמור להיות ברור מספיק כדי לשחזר אילו אינטראקציות או תגובות התרחשו באיזה סדר. זה עוזר באיתור באגים!

כתיבת reducers תמציתיים עם Immer

בדיוק כמו עם עדכון הstates ו-מערכים כדי. כאן, useImmerReducer תוכל לשנות את הstate עם הקצאת push או arr[i] =:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

אפשר להיות טהורים, אז הם לא צריכים לעבור תנועה בstate. אבל אני מספקת לך אובייקט טיוטה מיוחד שבטוח מוטציה. מתחת למכסה המנוע, Immer תיצור עותק של הstate שלך עם השינויים שביצעת ב’טיוטה’. אתה יכול לשנות את הטיעון הראשון שלהם ואינם צריכים להחזיר מצב.

Recap

  • להמיר מ’useState’ ל’useReducer’:
    1. שיגור פעולות ממטפלי אירועים.
    2. כתוב פונקציית reducer שמחזירה את הstate הבא עבור מצב ופעולה נתונים.
    3. החלף את ‘useState’ ב-‘useReducer’.
  • reducer דורשים ממך לכתוב קצת יותר קוד, אבל הם עוזרים בניפוי באגים ובדיקות.
  • reducer חייבים להיות טהורים.
  • כל פעולה מתארת ​​אינטראקציה של משתמש בודד.
  • השתמש ב-Immer אם אתה רוצה לכתוב reducer נוסח מוטציה.

Challenge 1 of 4:
שליחת פעולות ממטפלי אירועים

נכון לעכשיו, למטפלים ב-ContactList.js וב-Chat.js יש הערות // TODO. זה מה שהקלדה בקלט לא עובדת, ולחיצה על הכפתורים לא משנה את הנמען שנבחר.

החלף את שני // TODO אלה בקוד כדי לשלוח את התאימות לפעולות. ראה את הרצון כדי ואת סוג הפעולות, בדוק את הreducer ב- messengerReducer.js. הreducer כבר כתוב כך שלא תתאים אותו. אתה רק צריך לשלוח את הפעולות ב-‘ContactList.js’ ו-‘Chat.js’.

import { useReducer } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';
import { initialState, messengerReducer } from './messengerReducer';

export default function Messenger() {
  const [state, dispatch] = useReducer(messengerReducer, initialState);
  const message = state.message;
  const contact = contacts.find((c) => c.id === state.selectedId);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedId={state.selectedId}
        dispatch={dispatch}
      />
      <Chat
        key={contact.id}
        message={message}
        contact={contact}
        dispatch={dispatch}
      />
    </div>
  );
}

const contacts = [
  {id: 0, name: 'Taylor', email: 'taylor@mail.com'},
  {id: 1, name: 'Alice', email: 'alice@mail.com'},
  {id: 2, name: 'Bob', email: 'bob@mail.com'},
];