স্টেট লজিকগুলোকে একটি Reducer-এ আলাদা করা
যেসব কম্পোনেন্টে অনেকগুলো স্টেট আপডেট বিভিন্ন ইভেন্ট হ্যান্ডলারের মধ্যে ছড়িয়ে থাকে, সেগুলো সময়ের সাথে সাথে পড়া এবং মেইনটেইন করা বেশ কঠিন হয়ে পড়তে পারে। এই ধরনের ক্ষেত্রে, আপনি আপনার কম্পোনেন্টের বাইরের একটি একক ফাংশনে সমস্ত স্টেট আপডেটের লজিকগুলো একত্রিত করতে পারেন। এই ফাংশনটিকে reducer বলা হয়।
এই চ্যাপ্টার থেকে আপনি যা শিখবেন
- রিডিউসার (reducer) ফাংশন কী এবং এটি কীভাবে কাজ করে?
useStateকে রিফ্যাক্টর করে কীভাবেuseReducer-এ রূপান্তর করতে হয়।- কখন
useStateএর পরিবর্তে রিডিউসার ব্যবহার করা উচিত। - রিডিউসার ভালোভাবে লেখার কিছু স্ট্যান্ডার্ড নিয়ম।
একটি Reducer-এর সাহায্যে স্টেট লজিক একত্রিত করা
ধরা যাক, আপনার একটি টাস্ক লিস্ট (Task list) আছে যেখানে আপনি নতুন টাস্ক যোগ (Add), এডিট (Edit), এবং ডিলিট (Delete) করতে পারেন।
সাধারণত useState ব্যবহার করলে কোডটি দেখতে এরকম হতে পারে:
import { useState } from 'react';
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 (
// ... UI Components
);
}এখানে প্রতিটি ইভেন্ট হ্যান্ডলার স্টেট আপডেট করার জন্য setTasks কল করছে। যখন আপনার কম্পোনেন্ট বড় হতে থাকে এবং লজিক আরও জটিল হয়, তখন এই স্টেট লজিকগুলো ট্র্যাক করা কষ্টকর হয়ে যায়।
কম্প্লেক্সিটি কমানোর জন্য, আমরা এই সমস্ত স্টেট লজিকগুলোকে কম্পোনেন্টের বাইরে একটি reducer ফাংশনে নিয়ে যেতে পারি। useState থেকে useReducer-এ যাওয়ার জন্য মাত্র ৩টি ধাপ অনুসরণ করতে হবে:
ধাপ ১: স্টেট সেট করা থেকে অ্যাকশন ডিসপ্যাচ (Dispatch) করায় পরিবর্তন
setTasks এর মাধ্যমে সরাসরি স্টেট আপডেট করার বদলে, আপনি একটি "অ্যাকশন" (Action) ডিসপ্যাচ (dispatch) করবেন।
অ্যাকশন হলো একটি সাধারণ জাভাস্ক্রিপ্ট অবজেক্ট যা বর্ণনা করে ব্যবহারকারী "কী করেছে"।
function handleAddTask(text) {
// setTasks(...) এর পরিবর্তে আমরা dispatch কল করবো
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}dispatch ফাংশনের ভিতরে যে অবজেক্টটি পাস করা হয় তাকে "action" বলা হয়।
অ্যাকশন অবজেক্টের সাধারণত একটি type প্রোপার্টি থাকে যা ইভেন্টটির ধরন বোঝায় (যেমন: 'added', 'changed', 'deleted'), এবং এর সাথে অন্যান্য প্রয়োজনীয় ডেটা থাকতে পারে।
ধাপ ২: একটি reducer ফাংশন লেখা
রিডিউসার ফাংশন হলো সেই জায়গা যেখানে আপনি আপনার সমস্ত স্টেট লজিক রাখবেন। এটি দুটি আর্গুমেন্ট গ্রহণ করে: ১. বর্তমান স্টেট (Current State) ২. অ্যাকশন অবজেক্ট (Action Object)
এবং এটি পরবর্তী স্টেট রিটার্ন করে। ��টি কম্পোনেন্টের বাইরে লিখতে হয়।
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);
}
}
}নোট: রিডিউসারের ভেতরে switch স্টেটমেন্ট ব্যবহার করা কনভেনশন (প্রথা)। আপনি চাইলে if/else ও ব্যবহার করতে পারেন, তবে switch পড়তে অনেক সহজ।
ধাপ ৩: আপনার কম্পোনেন্ট থেকে reducer ব্যবহার করা
সবশেষে, আপনাকে আপনার কম্পোনেন্টে useReducer হুকটি ইম্পোর্ট করতে হবে এবং এটি ব্যবহার করতে হবে।
import { useReducer } from 'react';এবার useState কে সরিয়ে দিন:
// const [tasks, setTasks] = useState(initialTasks); এবং তার জায়গায় useReducer ব্যবহার করুন:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);useReducer হুকটি useState এর মতই একটি অ্যারে রিটার্ন করে। প্রথম ভ্যালুটি হলো বর্তমান স্টেট (tasks), এবং দ্বিতীয়টি হলো dispatch ফাংশন (যাতে আমরা অ্যাকশন পাঠাতে পারি)।
useState বনাম useReducer
কখন কোনটি ব্যবহার করবেন? নিচে একটি তুলনা দেওয়া হলো:
- কোড সাইজ (Code size):
useStateব্যবহার করলে শুরুতে কোড কম লিখতে হয়। কিন্তু যদি অনেকগুলো ইভেন্ট হ্যান্ডলার থাকে, তখনuseReducerকোডকে অনেক বেশি গোছানো এবং পরিষ্কার রাখে। - পঠনযোগ্যতা (Readability): লজিক সহজ হলে
useStateভালো। কিন্তু স্টেট আপডেট করার লজিক যখন অনেক জটিল হয়ে যায়, তখনuseReducerএর সাহায্যে "কী ঘটলো" (actions) এবং "কীভাবে স্টেট আপডেট হলো" (reducer) - এই দুটি বিষয় আলাদা করা যায়, যা কোড বোঝাকে সহজ করে। - ডিবাগিং (Debugging): রিডিউসার ফাংশনের ভিতরে একটি
console.logবসালে আপনি প্রতিটি স্টেট আপডেট এবং অ্যাকশন সহজেই দেখতে পারবেন। যা বড় অ্যাপ্লিকেশন ডিবাগ করতে দারুণ সাহায্য করে। - টেস্টিং (Testing): রিডিউসার একটি সাধারণ (pure) জাভাস্ক্রিপ্ট ফাংশন যা কম্পোনেন্টের বাইরে থাকে। তাই একে কম্পোনেন্ট রেন্ডার না করেও খুব সহজে আলাদাভাবে টেস্ট (Unit Test) করা যায়।
Reducer লেখার গুরুত্বপূর্ণ নিয়মাবলী
১. Reducers অবশ্যই Pure Function হতে হবে: ইনপুট একই হলে এর আউটপুট সবসময় একই হতে হবে। রিডিউসারের ভেতর থেকে কখনোই বাহ্যিক কোনো ভ্যারিয়েবল পরিবর্তন (mutate) করা, API কল করা বা কোনো সাইড ইফেক্ট (side effect) তৈরি করা উচিত নয়।
২. প্রতিটি অ্যাকশন একটি একক ইউজার ইন্টারঅ্যাকশন বর্ণনা করে: যদি ব্যবহারকারী "Reset" বাটন ক্লিক করে যাতে ৫টি ফিল্ড রিসেট হয়, তবে ৫টি আলাদা অ্যাকশনের বদলে একটি মাত্র reset_form অ্যাকশন ডিসপ্যাচ হওয়া উচিত। এতে স্টেট ম্যানেজমেন্ট অনেক বেশি নির্ভুল হয়।
Immer এর সাহায্যে সংক্ষিপ্ত Reducer লেখা
যেহেতু রিডিউসারের ভেতরে স্টেট মিউটেট (mutate) করা নিষেধ (যেমন tasks.push() ব্যবহার করা যাবে না), তাই নেস্টেড অবজেক্ট বা অ্যারে আপডেট করা একটু ঝামেলার মনে হতে পারে।
আপনি চাইলে use-immer লাইব্রেরির useImmerReducer ব্যবহার করতে পারেন। এটি আপনাকে মিউটেটিং সিনট্যাক্স লিখতে দেয়, কিন্তু ব্যাকগ্রাউন্ডে সেটিকে ইমিউটেবল হিসেবেই হ্যান্ডেল করে!
import { useImmerReducer } from 'use-immer';
function tasksReducer(draft, action) {
switch (action.type) {
case 'added': {
draft.push({
id: action.id,
text: action.text,
done: false,
});
break;
}
case 'changed': {
const index = draft.findIndex((t) => t.id === action.task.id);
draft[index] = action.task;
break;
}
case 'deleted': {
return draft.filter((t) => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}সারসংক্ষেপ (Summary)
useReducerহলোuseStateএর একটি বিকল্প যা জটিল স্টেট লজিক ম্যানেজ করতে ব্যবহৃত হয়।- রিডিউসার ব্যবহার করার জন্য:
১. কম্পোনেন্ট থেকে অ্যাকশন dispatch করুন।
২. একটি reducer ফাংশন লিখুন যা বর্তমান স্টেট এবং অ্যাকশন গ্রহণ করে পরবর্তী স্টেট রিটার্ন করবে।
৩. কম্পোনেন্টের ভেতর
useStateএর বদলেuseReducerকল করুন। - রিডিউসার ফাংশনকে সবসময় pure রাখতে হবে এবং ইমিউটেবল উপায়ে স্টেট আপডেট করতে হবে।