הגדלה עם reducer ו-הקשר

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

You will learn

  • איך לשלב reducer עם הקשר
  • איך להימנע ממעבר state ומשלוח דרך props
  • איך לשמור על הקשר והיגיון מצב בקובץ נפרד

שילוב של reducer עם הקשר

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

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>Day off in Kyoto</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: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

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

לדוגמה, TaskApp מעביר רשימה של משימות ואת מטפלי האירועים לTaskList:

<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>

ו-‘TaskList’ מעביר את מטפלי האירועים ל-‘Task’:

<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>

בדוגמה קטנה כמו זו, זה עובד היטב, אבל אם יש לך עשרות או מאות רכיבים באמצע, העברת כל הstates והפונקציות יכול להיות די מתסכל!

זו הסיבה, כחלופה להעברתם דרך props, אולי תרצה להכניס גם את מצב משימות וגם את פונקציית שיגור להקשר. בדרך זו, כל רכיב מתחת ל-TaskApp בעץ יכול לקרוא את המשימות ולשלוח פעולות ללא ” תרגיל חוזר”.

הנה איך אתה יכול לשלב reducer עם הקשר:

  1. צור את ההקשר.
  2. הצב מצב ושליחה בהקשר.
  3. השתמש בהקשר בכל מקום בעץ.

שלב 1: צור את ההקשר

ה- ‘useReducer’ Hook מחזיר את ה’משימות’ הנוכחיות ואת הפונקציה ‘שיגור’ המאפשרת לך לעדכן אותן:

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

כדי להעביר אותם בעץ, תיצור שני הקשרים נפרדים:

  • TasksContext מספק את רשימת המשימות הנוכחית.
  • TasksDispatchContext מספק את הפונקציה המאפשרת לרכיבים לשלוח פעולות.

ייצא אותם מקובץ נפרד כדי שתוכל לייבא אותם מאוחר יותר מקבצים אחרים:

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

כאן, אתה מעביר את ‘null’ כערך ברירת המחדל לשני ההקשרים. הערכים בפועל יסופקו על ידי רכיב ‘TaskApp’.

שלב 2: הכנס מצב ושליחה להקשר

כעת אתה יכול לייבא את שני ההקשרים ברכיב ‘TaskApp’ שלך. קח את ה’משימות’ וה’שליחות’ שהוחזרו על ידי ‘useReducer()’ ו-ספק אותן לעץ המלא למטה:

import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}

לעת עתה, אתה מעביר את המידע הן באמצעות props והן בהקשר:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksContext, TasksDispatchContext } from './TasksContext.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 (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        <h1>Day off in Kyoto</h1>
        <AddTask
          onAddTask={handleAddTask}
        />
        <TaskList
          tasks={tasks}
          onChangeTask={handleChangeTask}
          onDeleteTask={handleDeleteTask}
        />
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

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: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

בשלב הבא, תסיר את העברת הprops.

שלב 3: השתמש בהקשר בכל מקום בעץ

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

<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>

במקום זאת, כל רכיב שזקוק לרשימת המשימות יכול לקרוא אותה מתוך ה-TaskContext:

export default function TaskList() {
const tasks = useContext(TasksContext);
// ...

כדי לעדכן את רשימת המשימות, כל רכיב יכול לקרוא את פונקציית ‘שיגור’ מהקשר ולקרוא לה:

export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
// ...

רכיב TaskApp אינו מעביר אף מטפל באירועים, ו-TaskList אינו מעביר אף מטפל באירועים לרכיב Task. כל רכיב קורא את ההקשר שהוא צריך:

import { useState, useContext } from 'react';
import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskList() {
  const tasks = useContext(TasksContext);
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <Task task={task} />
        </li>
      ))}
    </ul>
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useContext(TasksDispatchContext);
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={e => {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      />
      {taskContent}
      <button onClick={() => {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}>
        Delete
      </button>
    </label>
  );
}

הstate עדיין “חיה” ברכיב TaskApp ברמה העליונה, המנוהל באמצעות useReducer. אבל משימות ומשלוח שלו זמינים כעת לכל רכיב למטה בעץ על ידי ייבוא ​​ושימוש בהקשרים אלו.

העברת כל החיווט לקובץ בודד

אתה לא חייב לעשות זאת, אבל אתה יכול עוד יותר לבטל את הרכיבים על ידי העברת הreducer וההקשר לקובץ בודד. נכון לעכשיו, TasksContext.js מכיל רק שתי הצהרות הקשר:

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

הקובץ הזה עומד להיות צפוף! אתה תעביר את הreducer לאותו קובץ. לאחר מכן תכריז על רכיב TasksProvider חדש באותו קובץ. רכיב זה יקשר את כל החלקים יחד:

  1. היא תנהל את הstate עם reducer.
  2. זה יספק את שני ההקשרים לרכיבים למטה.
  3. זה ייקח ילדים כprops כך שתוכל להעביר אליו JSX.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}

זה מסיר את כל המורכבות והחיווט מרכיב ה-TaskApp שלך:

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

אתה יכול גם לייצא פונקציות ש_משתמשות_ בהקשר מתוך TasksContext.js:

export function useTasks() {
return useContext(TasksContext);
}

export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}

כאשר רכיב צריך לקרוא את ההקשר, הוא יכול לעשות זאת באמצעות הפונקציות הבאות:

const tasks = useTasks();
const dispatch = useTasksDispatch();

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

import { useState } from 'react';
import { useTasks, useTasksDispatch } from './TasksContext.js';

export default function TaskList() {
  const tasks = useTasks();
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <Task task={task} />
        </li>
      ))}
    </ul>
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useTasksDispatch();
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={e => {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      />
      {taskContent}
      <button onClick={() => {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}>
        Delete
      </button>
    </label>
  );
}

אתה יכול לחשוב על ‘TasksProvider’ כחלק מהמסך שיודע להתמודד עם משימות, על ‘useTasks’ כדרך לקרוא אותן, ועל ‘useTasksDispatch’ כדרך לעדכן אותן מכל רכיב למטה בעץ.

Note

פונקציות כמו useTasks ו-useTasksDispatch נקראות Custom Hooks. הפונקציה שלך נחשבת Hook מותאמת אישית אם השם שלה מתחיל ב-use. זה מאפשר לך להשתמש ב-Hooks אחרים, כמו ‘useContext’, בתוכו.

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

Recap

  • אתה יכול לשלב reducer עם הקשר כדי לאפשר לכל רכיב לקרוא ולעדכן מצב מעליו.
  • כדי לספק מצב ופונקציית השיגור לרכיבים הבאים:
    1. צור שני הקשרים (עבור מצב ופונקציות שיגור).
    2. ספק את שני ההקשרים מהרכיב שמשתמש בreducer.
    3. השתמש בכל אחד מההקשרים מרכיבים שצריכים לקרוא אותם.
  • אתה יכול לשחרר עוד יותר את הרכיבים על ידי העברת כל החיווט לקובץ אחד.
    • אתה יכול לייצא רכיב כמו TasksProvider שמספק הקשר.
    • אתה יכול גם לייצא Hooks מותאמים אישית כמו useTasks ו-useTasksDispatch כדי לקרוא אותו.
  • אתה יכול לקבל הרבה זוגות reducerי הקשר כמו זה באפליקציה שלך.