חילוץ לוגיקת state ל-מחמצת

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

You will learn

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

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

כמה שהרכיבים שלך גדלים במורכבות, זה יהיה קשה יותר לראות במבט אחד את כל הדרכים השונות שבהן מצב הרכיב מתעדכן. לדוגמה, הרכיב ‘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 הזה לפונקציה יחידה מחוץ לרכיב שלך, המכונה “מפחית”.

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

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

שלב 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) נקרא כאשר משתמש לוחץ על “מחק”.

ניהול מצב עם מפחית שונה במקצת מ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: מפחית כתוב פונקציית

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

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

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

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

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

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

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);
}
}

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

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

למה קוראים למפחיתים כך?

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

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

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

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

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

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: השתמש במחמצת מהרכיב שלך

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

import { useReducer } from 'react';

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

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

עם ‘useReducer’ כך:

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

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

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

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

וזה מחזיר:

  1. ערך ממלכתי
  2. פונקציית שיגור (כדי “לשלוח” פעולות משתמש ל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},
];

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

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},
];

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

השוואת useState ו-useReducer

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

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

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

כתיבה מפחיתה היטב

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

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

כתיבת מצמצמים תמציתיים עם 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. שיגור פעולות ממטפלי אירועים.
  1. כתוב פונקציית מפחית שמחזירה את הstate הבא עבור מצב ופעולה נתונים.
  2. החלף את ‘useState’ ב-‘useReducer’.
  • מפחית דורשים ממך לכתוב קצת יותר קוד, אבל הם עוזרים בניפוי באגים ובדיקות.
  • מפחית חייב להיות טהורים.
  • כל פעולה מתארת ​​אינטראקציה של משתמש בודד.
  • השתמש ב-Immer אם אתה רוצה לכתוב מפחית נוסח מוטציה.

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

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

החלף את שני // TODO אלה בקוד כדי לשלוח את התאימות לפעולות. ראה את הרצון כדי ואת סוג הפעולות, בדוק את המפחית ב-messengerReducer.js. המפחית כבר כתוב כך שלא תתאים אותו. אתה רק צריך לשלוח את הפעולות ב-‘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'},
];