توسعه با Reducer و Context

reducer به شما این امکان رو میدهد که بتوانید state کامپوننت مورد نظرتان را یکپارچه کنید. Context این امکان رو به شما می دهد که اطلاعات را به کامپوننت های دیگر انتقال دهید. شما میتوانید با ادغام reducers و context با هم دیگر state محتواهای پیچیده را مدیریت کنید.

You will learn

  • چگونه reducer و context را با یکدیگر ترکیب کنید
  • چگونه از پاسکاری state و dispatch به وسیله props جلوگیری کنید
  • چگونه از context و منطق state در فایل های جداگانه، نگهداری کنید

ترکیب یک reducer با context

در این مثال از معرفی reducers، استیت توسط یک 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 به کوتاه و مختصر بودن event handler ها کمک میکند. به هر حال زمانی که مقیاس برنامه شما بزرگتر می شود، ممکن است با مشکل دیگری رو به رو شوید. در حال حاضر، state tasks و تابع dispatch تنها در بالاترین لایه‌ی کامپوننت ‍‍‍TaskApp‍‍‍ در دسترس هستند. برای اینکه باقی کامپوننت ها نیز قابلیت دسترسی و تغییر لیست تسک ها را داشته باشند، باید state فعلی و event handler هایی که به عنوان props آن را تغییر میدهند، را منتقل کنید.

به عنوان مثال، TaskApp یک لیست از tasks و event handlers را به TaskList منتقل می‌کند:

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

و TaskList event handler ها را به Task ارسال می کند:

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

در مثال های کوچک مانند این مساله، این روش خوب کار می‌کند، اما اگر شما ده‌ها یا صدها کامپوننت در میانه داشته باشید، انتقال تمام state و توابع می‌تواند کاری خسته‌کننده باشد!

در همین راستا، به عنوان یک جایگزین برای انتقال آنها از طریق props، ممکن است بخواهید هم state tasks و هم تابع dispatch را در context قرار دهید . به این ترتیب، هر کامپوننتی زیر TaskApp در درخت می‌تواند وظایف را بخواند و اقدامات dispatch را بدون تکرار “prop drilling” انجام دهد.

در ادامه نحوه ترکیب یک reducer با context را مشاهده می‌کنید:

  1. ایجاد context.
  2. قراردادن state و dispatch در context.
  3. استفاده از context در هر قسمتی از این درخت.

گام 1: ایجاد context

هوک useReducer مقدار tasks را برمیگرداند و تابع dispatch امکان به روزرسانی آنها را فراهم می‌کند:

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

برای انتقال آنها در درخت، شما میتوانید دو context جداگانه ایجاد کنید:

  • TasksContext لیستی از task ها را فراهم می‌کند.
  • TasksDispatchContext تابعی را می‌سازد که کامپوننت ها بتوانند اکشن ها را dispatch کنند.

آن‌ها را از یک فایل جداگانه export کنید تا بتوانید بعداً آن‌ها را از فایل‌های دیگر import کنید:

import { createContext } from 'react';

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

در اینجا، شما null را به عنوان مقدار پیش‌فرض به هر دو context ارسال می‌کنید. مقادیر واقعی توسط کامپوننت TaskApp ارائه خواهد شد.

گام 2: قراردادن state و dispatch در context

الان می‌توانید هر دوی context ها را در کامپوننت TaskApp import کنید. tasks و dispatch برگردانده شده از 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 و هم در context ارسال می‌کنید:

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: استفاده از context در هر قسمتی از درخت

اکنون نیازی به انتقال لیست task ها یا event handler به پایین درخت ندارید:

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

به جای آن، هر کامپوننتی که به لیست tasks نیاز دارد، می‌تواند آن را از ‍‍‍TaskContext بخواند:

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

برای به‌روزرسانی لیست tasks، هر کامپوننت می‌تواند تابع dispatch را از context بخواند و آن را فراخوانی کند:

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 دیگر هیچ event handler را به پایین منتقل نمی‌کند، و TaskList نیز هیچ event handler را به کامپوننت Task منتقل نمی‌کند. هر کامپوننت context مورد نیاز خود را می‌خواند:

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

وضعیت هنوز در کامپوننت بالادستی TaskApp “زندگی می‌کند” و با useReducer مدیریت می‌شود. اما tasks و dispatch آن اکنون توسط هر کامپوننت زیرین در درخت با وارد کردن و استفاده از این contextها در دسترس است.

جابجایی تمام اتصالات به یک فایل

شما نیازی به انجام این کار ندارید، اما می‌توانید با جابجایی هم reducer و هم context را در یک فایل تمیزتر کنید. در حال حاضر، TasksContext.js فقط شامل دو اعلان context است:

import { createContext } from 'react';

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

این فایل در حال شلوغ و پیچیده شدن می‌باشد! reducer را به فایل مشابه منتقل کنید، سپس یک کامپوننت ‍TasksProvider را در همان فایل ایجاد کنید. این کامپوننت تمام قطعات را به هم متصل می کند:

  1. state را همراه با یک reducer مدیریت می‌کند.
  2. هر دو context را برای کامپوننت‌های زیرین فراهم می‌کند.
  3. children را به عنوان یک prop منتقل میکند تا بتوانید 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>
  );
}

همچنین می‌توانید توابعی را که از context استفاده می‌کنند از TasksContext.js export کنید.

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

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

هنگامی که یک کامپوننت نیاز به خواندن context دارد، می‌تواند این کار را از طریق این توابع انجام دهد.

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

این به هیچ وجه رفتار را تغییر نمی‌دهد، اما به شما امکان می‌دهد که در آینده این context را بیشتر تقسیم کنید یا برخی منطق به این توابع اضافه کنید. اکنون تمام اتصالات context و 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 به عنوان یک بخش از صفحه فکر کنید که نحوه برخورد با task ها را می‌شناسد، useTasks را به عنوان یک روش برای خواندن آنها و useTasksDispatch را به عنوان یک راه برای به‌روزرسانی آنها از هر کامپوننتی در زیر درخت، در نظر بگیرید.

Note

توابعی مانند useTasks و useTasksDispatch ، Custom Hooks نامیده می‌شوند. اگر نام یک تابع با use شروع شود، تابع شما به عنوان یک Custom Hook محسوب می‌شود. این قابلیت را برای استفاده از دیگر Hook ها مانند useContext داخل آن نیز ممکن می‌کند.

هر چقدر که برنامه‌ی شما بزرگ‌تر می‌شود، ممکن است چندین جفت context-reducer هایی را داشته باشید. این روشی قدرتمند برای افزایش مقیاس برنامه شماست و به شما امکان می‌دهد که state را در درخت پخش کنید بدون نیاز به انجام کار بیشتر، هر زمان که می‌خواهید به داده‌های درون درخت دسترسی داشته باشید.

Recap

  • شما می‌توانید یک reducer را با context ترکیب کنید تا هر کامپوننتی بتواند state را خوانده و به‌روزرسانی کند.
  • برای انتشار state و تابع dispatch به کامپوننت‌های پایین‌تر:
    1. دو context (برای state و برای توابع dispatch) ایجاد کنید.
    2. هر دو context را با توجه به کامپوننتی که از reducer استفاده می‌کند، ایجاد کنید.
    3. از context ای استفاده کنید که مرتبط با کامپوننت‌هایی که نیاز به خواندن دارند، باشد.
  • می‌توانید با جابجایی همه اتصالات در یک فایل، کامپوننت‌ها را بهتر مدیریت کنید.
    • می‌توانید یک کامپوننت مانند TasksProvider را که context فراهم می‌کند، export کنید.
    • همچنین می‌توانید Custom Hookهایی مانند useTasks و useTasksDispatch برای خواندن آن استفاده کنید.
  • می‌توانید در برنامه‌ی خود چندین جفت context-reducer مانند این داشته باشید.