Scaling Up with Reducer and Context

Reducers একটি কম্পোনেন্টের state আপডেট লজিক সংক্ষেপণ করতে সাহায্য করে। Context আপনাকে তথ্য অন্যান্য কম্পোনেন্টগুলির গভীরে পাঠানোর সুযোগ দেয়। আপনি reducers এবং context দুটি একসাথে সংমিলিত করে একটি জটিল স্ক্রিনের state ব্যবস্থাপনা করতে পারেন ।

যা যা আপনি শিখবেন

  • কিভাবে reducer কে context এর সাথে সংযুক্ত করতে হয় ।
  • কিভাবে state এবং dispatch কে props এর মাধ্যমে পাঠানো থেকে বিরত থাকা যায় ।
  • কিভাবে context এবং state এর যুক্তিকে ভিন্ন ফাইলে রাখা যায় ।

Context এর সাথে 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 ইভেন্ট হ্যান্ডলারগুলি ছোট এবং সংক্ষিপ্ত রাখতে সাহায্য করে । তবে, আপনার অ্যাপ্লিকেশন বাড়তে শুরু করলে, আপনি আরও একটি সমস্যায় পরে যেতে পারেন । বর্তমানে, tasks state এবং dispatch ফাংশনটি শুধুমাত্র শীর্ষ-স্তরের TaskApp কম্পোনেন্টে উপলব্ধ রয়েছে। অন্য কম্পোনেন্টগুলিকে টাস্কের তালিকা পড়তে অথবা তা পরিবর্তন করতে দিতে হলে, আপনাকে বর্তমান state এবং তা পরিবর্তন করার ইভেন্ট হ্যান্ডলারগুলি স্পষ্টভাবে props হিসেবে পাঠাতে হবে।

উদাহরণস্বরূপ, TaskApp টাস্কের তালিকা এবং ইভেন্ট হ্যান্ডলারগুলি TaskList এ পাঠিয়ে দেয়:

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

এবং TaskList ইভেন্ট হ্যান্ডলারগুলোকে Task এ পাঠিয়ে দেয়ঃ

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

একটি ছোট উদাহরনে এটি ভালো কাজ করে, কিন্তু যদি এর মাঝে আপনার দশ বা শতাধিক কম্পোনেন্ট থাকে, তাহলে সকল state এবং ফাংশনগুলিকে পাঠানো অনেক বিরক্তিকর হতে পারে ।

এই কারনে, props এর মাধমে পাঠানোর বিকল্প হিসেবে, আপনি সমস্ত tasks স্টেট এবং dispatch ফাংশনকে context এর মধ্যে রাখতে পারেন । এইভাবে, TaskApp এর নীচে যেকোনো কম্পোনেন্ট রুটে আপনি “prop drilling” এর পুনরাবৃত্তি ছাড়াই task পড়তে এবং একশনগুলিকে dispatch করতে পারবেন ।

যেভাবে আপনি reducer এবং context এর সংযুক্তি করতে পারেনঃ

  1. Context তৈরি করুন।
  2. state এবং dispatch কে Context এর ভেতরে রাখুন
  3. Context কে যেকোনো কম্পোনেন্ট রুটে ব্যবহার করুন।

ধাপ ১: Context তৈরি করুন

useReducer হুক আপনাকে বর্তমান tasks এবং তা আপডেট করার জন্য dispatch ফাংশনকে রিটার্ন করে ।

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

To pass them down the tree, you will create two separate contexts: তাদেরকে ট্রি-এর নিচে পাঠানোর জন্য আপনি দুটি ভিন্ন context তৈরি করবেন ।

  • TasksContext বর্তমান tasks তালিকা প্রদান করে ।
  • TasksDispatchContext একটি ফাংশন প্রদান করে যা কম্পোনেন্টগুলিকে একশনগুলি dispatch করতে দেয় ।

এদেরকে একটি আলাদা ফাইলে এক্সপোর্ট করুন যাতে আপনি পরবর্তিতে অন্য ফাইলে ইম্পোর্ট করতে পারেন:

import { createContext } from 'react';

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

এখানে আপনি null কে ডিফল্ট মান হিসেবে দুটি context এ পাঠাচ্ছেন । আসল মানগুলি TaskApp এর মাধ্যমে সরাসরি প্রদান হবে ।

ধাপ ২: State এবং dispatch কে context এর ভেতরে রাখুন

এখন আপনি দুটো context কে TaskApp কম্পোনেন্টে ইম্পোর্ট করতে পারেন । useReducer() এর রিটার্ন করা tasks এবং dispatch কে গ্রহণ করুন এবং এদেরকে নিচের সম্পূর্ন ট্রিতে প্রদান করুন:

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

পরবর্তী ধাপে, আপনি prop পাঠানো মুছে ফেলবেন ।

ধাপ ৩: ট্রি এর যেকোনো জায়গায় context ব্যবহার করুন

এখন আপনাকে আর task এর তালিকা অথবা event handlers কে ট্রি এর নিচে পাঠাতে হবেনা :

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

এর পরিবর্তে যেকোনো কম্পোনেন্ট যার task তালিকা দরকার হবে সে তা TaskContext থেকে পড়তে পারবে ।

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

Task তালিকা হালনাগাদ করার জন্য যেকোনো কম্পোনেন্ট dispatch ফাংশনকে context থেকে পড়তে পারেন এবং call করতে পারেন ।

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 handlers কে নিচে পাঠায় না এবং TaskList কোনো event handlers কে 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>
  );
}

State টি এখনো টপ-লেভেল TaskApp কম্পোনেন্টেই অবস্থান করছে, useReducer এর ব্যবস্থাপনায়। কিন্তু এর tasks এবং dispatch এখন ট্রিয়ের নিচের প্রতিটি কম্পোনেন্ট পাওয়া যাবে ইম্পোর্টিং এবং এই context গুলিকে ব্যবহারের মাধ্যমে।

Moving all wiring into a single file

You don’t have to do this, but you could further declutter the components by moving both reducer and context into a single file. Currently, TasksContext.js contains only two context declarations:

import { createContext } from 'react';

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

This file is about to get crowded! You’ll move the reducer into that same file. Then you’ll declare a new TasksProvider component in the same file. This component will tie all the pieces together:

  1. It will manage the state with a reducer.
  2. It will provide both contexts to components below.
  3. It will take children as a prop so you can pass JSX to it.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

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

This removes all the complexity and wiring from your TaskApp component:

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

You can also export functions that use the context from TasksContext.js:

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

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

When a component needs to read context, it can do it through these functions:

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

This doesn’t change the behavior in any way, but it lets you later split these contexts further or add some logic to these functions. Now all of the context and reducer wiring is in TasksContext.js. This keeps the components clean and uncluttered, focused on what they display rather than where they get the data:

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

You can think of TasksProvider as a part of the screen that knows how to deal with tasks, useTasks as a way to read them, and useTasksDispatch as a way to update them from any component below in the tree.

খেয়াল করুন

Functions like useTasks and useTasksDispatch are called Custom Hooks. Your function is considered a custom Hook if its name starts with use. This lets you use other Hooks, like useContext, inside it.

As your app grows, you may have many context-reducer pairs like this. This is a powerful way to scale your app and lift state up without too much work whenever you want to access the data deep in the tree.

পুনরালোচনা

  • You can combine reducer with context to let any component read and update state above it.
  • To provide state and the dispatch function to components below:
    1. Create two contexts (for state and for dispatch functions).
    2. Provide both contexts from the component that uses the reducer.
    3. Use either context from components that need to read them.
  • You can further declutter the components by moving all wiring into one file.
    • You can export a component like TasksProvider that provides context.
    • You can also export custom Hooks like useTasks and useTasksDispatch to read it.
  • You can have many context-reducer pairs like this in your app.