State Logic কে একটি Reducer এ স্থানান্তর করা
একাধিক event handler এ ছড়িয়ে থাকা একাধিক state update ওয়ালা কম্পোনেন্টগুলো দুঃসহ হয়ে যেতে পারে। এসব ক্ষেত্রে, আপনি সকল state update logic কে আপনার কম্পোনেন্টের বাইরে একটিমাত্র function এ একত্রিত করতে পারেন, যাকে বলা হয় reducer।
যা যা আপনি শিখবেন
- reducer function বলতে কী বুঝায়
- কিভাবে
useState
কে গুছিয়েuseReducer
এ পরিণত করা যায় - কখন reducer ব্যবহার করতে হয়
- কীভাবে একে ভালভাবে লিখতে হয়
State logic কে একটি reducer এ একত্র করুন
ধীরে ধীরে যখন আপনার কম্পোনেন্টগুলোর জটিলতা বাড়তে থাকে, তখন এক নজর দেখে এটা বোঝা কঠিন হয়ে যেতে পারে যে কতোনা উপায়ে একটা কম্পোনেন্টের state আপডেট হতে পারে। উদাহরণস্বরূপ, নিচের TaskApp
কম্পোনেন্টটি tasks
নামক array কে state হিসেবে ধারণ করে, আর কোনো task কে add, edit, remove করার জন্য তিনটি ভিন্ন ভিন্ন event handler এর ব্যবহার করেঃ
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}, ];
এর প্রতিটি event handler state কে আপডেট করার জন্য setTasks
কে call করে। ধীরে ধীরে যখন এ কম্পোনেন্টটি আকারে বাড়তে থাকবে, তখন সাথে সাথে এর ভিতরকার state logic ও বাড়তে থাকবে এবং জটিলতর হতে থাকবে। এই জটিলতা কমাতে এবং আপনার সব state logic একটি সহজে-পাওয়া-যায় এমন জায়গায় রাখতে, আপনি ঐসব state logic কে আপনার কম্পোনেন্টের বাইরে একটি function এ স্থানান্তর করতে পারেন, যে function টিকে বলা হয় “reducer”.
Reducer হলো state হ্যান্ডেল করার একটি বিকল্প পদ্ধতি। আপনি useState
থেকে useReducer
এ তিনটি ধাপে স্থানান্তর করতে পারেনঃ
- state কে set করার বদলে action কে dispatch করতে শুরু করুন।
- একটি reducer function লিখুন।
- reducer টিকে আপনার কম্পোনেন্ট থেকে ইউজ করুন।
ধাপ ১ঃ State কে set করার বদলে action কে dispatch করতে শুরু করুন
State কে set করার মাধ্যমে আপনার event handler গুলো বর্তমানে নির্ধারণ করছে যে কী করতে হবেঃ
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 সেট করার logic দূর করে দিন। এখন আপনার কাছে যা বাকি থাকবে তা হলোঃ
- ইউজার যখন “Add” প্রেস করে তখন call করা হয়
handleAddTask(text)
। - ইউজার যখন “Save” প্রেস করে কিংবা কোনো task কে toggle (বা edit) করে তখন call করা হয়
handleChangeTask(task)
। - ইউজার যখন “Delete” প্রেস করে তখন call করা হয়
handleDeleteTask(taskId)
।
Reducer দিয়ে state ম্যানেজ করা, state সেট করা থেকে কিছুটা ভিন্ন জিনিস। React কে state সেট করার মাধ্যমে “কী করতে হবে” না বলে, আপনি আপনার event handler গুলো থেকে “action” গুলোকে dispatch করার মাধ্যমে ঠিক করে দেন “ইউজার এইমাত্র কী করলো”। (আর state update logic অন্য আরেক জায়গায় থাকবে!) তাই একটি event handler এর মাধ্যমে “tasks
সেট করার” পরিবর্তে, আপনি “একটি task add/change/delete করার” action(কাজ) dispatch করবেন। আর এই পদ্ধতিটি ইউজারের আকাঙ্ক্ষাকে বেশি বর্ণনা করে।
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
এর কাছে যে object টি pass করেন, তাকে একটি “action” বলেঃ
function handleDeleteTask(taskId) {
dispatch(
// "action" object:
{
type: 'deleted',
id: taskId,
}
);
}
এটি একটি সাধারণ JavaScript object। এর মধ্যে কী রাখতে হবে সেটা আপনার উপর, তবে স্বাভাবিকভাবে এর মধ্যে কী ঘটলো(what happened) সে ব্যপারে ন্যূনতম ইনফর্মেশন থাকতে হবে। (আর আপনি dispatch
ফাংশনটিকে পরবর্তী একটি ধাপে যুক্ত করবেন।)
ধাপ ২ঃ একটি reducer function লিখুন
একটি reducer function হলো যেখানে আপনি আপনার state লজিক রাখবেন। এটি দুটি argument নেয়, বর্তমান state এবং action অবজেক্ট, অতঃপর এটি পরবর্তী state কে return করেঃ
function yourReducer(state, action) {
// return next state for React to set
}
আপনি reducer থেকে যা return করবেন, React সেটিকে state হিসেবে সেট করে দিবে।
এই উদাহরণে, state সেট করার লজিককে event handlers থেকে একটি reducer function এ সরাতে, আপনারঃ
- বর্তমান state (
tasks
) কে প্রথম argument হিসেবে declare করতে হবে। action
অবজেক্টকে দ্বিতীয় argument হিসেবে declare করতে হবে।- reducer থেকে পরবর্তী state কে return করতে হবে। (যেটিকে React পরবর্তী state হিসেবে সেট করবে)
সব state সেট করার লজিক reducer function এ সরানোর পর এমন দেখাবেঃ
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);
}
}
যেহেতু reducer function টি state (tasks
) কে একটি argument হিসেবে নিচ্ছে, আপনি একে আপনার কম্পোনেন্টের বাইরে declare করতে পারবেন। এটা indentation level কমিয়ে আনে এবং আপনার কোডকে পড়তে সহজ করে।
গভীরভাবে জানুন
যদিও reducer আপনার কম্পোনেন্টের ভিতরে কোডের পরিমাণ কমাতে পারে, কিন্তু reducer নাম দেয়ার পিছনে আসল রহস্য হচ্ছে reduce()
অপারেশন, যেটি আপনি array এর উপর প্রয়োগ করতে পারেন।
reduce()
অপারেশনটি আপনাকে একটি array এর একাধিক ভ্যালুকে “একত্র করে” একটি ভ্যালুতে নিয়ে আনার ক্ষমতা দেয়ঃ
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5
reduce
কে আপনি যে ফাংশনটি পাস করেন তাকে বলা হয় “reducer”। এটা গ্রহণ করে এখন অবধি রেজাল্ট এবং বর্তমান item, তারপর এটা return করে পরবর্তী রেজাল্ট। React reducer ও এর অনুরূপঃ গ্রহণ করে এখন অবধি state এবং action, এবং return করে পরবর্তী state। এমন করে, সময়ের সাথে সেটি action সমূহকে কে state হিসেবে একত্র করে।
এমনকি আপনি reduce()
মেথডটি দিয়েও একটি initialState
এবং একটি actions
এর array থেকে সর্বশেষ state বের করতে পারবেন, তার জন্য মেথডটিকে আপনার reducer ফাংশনটি পাস করতে হবেঃ
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 যেভাবে করে দেয় তার মতোই!
ধাপ ৩ঃ আপনার কম্পোনেন্টে reducer টি ব্যাবহার করুন
সবশেষে, আপনার tasksReducer
টিকে আপনার কম্পোনেন্টের সাথে সংযুক্ত করে দিতে হবে। React থেকে useReducer
হুকটি import করুনঃ
import { useReducer } from 'react';
অতঃপর আপনি useState
কে সরিয়ে দিতে পারেনঃ
const [tasks, setTasks] = useState(initialTasks);
useReducer
দিয়ে, ঠিক এভাবেঃ
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer
হুকটি অনেকটা useState
মতো—আপনার অবশ্যই একে একটি initial state (স্টেটের প্রাথমিক ভ্যালু) পাস করতে হবে আর এটি return করে state এর ভ্যালু এবং state কে সেট করার একটি পদ্ধতি (এক্ষেত্রে, dispatch ফাংশন)। কিন্তু এটি (useState
থেকে) একটু আলাদা।
useReducer
হুকটি দুটি argument নেয়ঃ
- একটি reducer function
- একটি initial state
আর এটি return করেঃ
- একটি state ভ্যালু
- একটি dispatch function (ইউজার actions কে reducer এর নিকট “dispatch বা প্রেরণ” করার জন্য)
এখন এটিকে পুরোপুরি সেট আপ করা হয়ে গেছে। এখানে, reducer টিকে component file এর নিচের দিকে declare করা হয়েছেঃ
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}, ];
যদি চান, তাহলে আপনি reducer টিকে ভিন্ন আরেকটি ফাইলেও নিতে পারেনঃ
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}, ];
যখন আপনি এমন করে separation of concern বজায় রাখবেন, কম্পোনেন্ট লজিক পড়াটা তখন সহজতর হবে। এখন event handler গুলো actions কে dispatch (প্রেরণ) করার মাধ্যমে শুধু কি ঘটলো সেটা নির্ধারণ করে, আর তার জবাবে reducer function টি নির্ধারণ করে কিভাবে state টি update হয়।
useState
এবং useReducer
এর তুলনা
Reducer এর যে একদম কোনো খারাপ দিক নেই এমনটি না! আপনি নিচের কয়েকটি উপায়ে উভয়ের মাঝে তুলনা করতে পারেনঃ
- কোডের দৈর্ঘ্য (Code size): সাধারণত,
useState
এর বেলায় আপনার শুরুতে কম কোড লেখা লাগে। আরuseReducer
এর বেলায়, আপনাকে একটি reducer function লেখা এবং actions কে dispatch করা উভয়টিই করতে হয়। তবে,useReducer
কোডের দৈর্ঘ্য কমাতে সহায়তা করতে পারে যদি কয়েকটি event handler একইভাবে state কে modify করে থাকে। - পড়ার সহজতা (Readability):
useState
পড়তে খুব সহজ যখন state update গুলো simple হয়। যখন তা জটিল হয়, তখনuseState
গুলো আপনার কম্পোনেটের কোডকে হিজিবিজি করে তোলে ও কোডে চোখ বুলানোটা কঠিনতর করে তোলে। এক্ষেত্রে,useReducer
আপনাকে লজিক আপডেট কিভাবে হলো (how) এবং event handler গুলোতে কি ঘটলো (what happened) পরিষ্কারভাবে আলাদা আলাদা রাখতে দেয়। - বাগ দূর করা (Debugging): যখন আপনার
useState
সংক্রান্ত কোনো bug থাকে, তখন কোথায় এবং কেনো স্টেটটিকে ভুলভাবে সেট করা হয়েছিলো এটা নির্ণয় করা কঠিন হয়ে উঠতে পারে।useReducer
এর ক্ষেত্রে, আপনি প্রত্যেক স্টেট আপডেট এবং কেনো (কোনaction
এর কারণে) তা ঘটলো সেটা দেখার জন্য reducer টিতে একটি console log যুক্ত করে দিতে পারেন। যদি প্রতিটিaction
সঠিক হয়ে থাকে, তখন আপনি বুঝে যাবেন যে ভুলটি আসলে reducer logic এর ভিতরে রয়েছে। তবে, আপনাকে এক্ষেত্রেuseState
এর থেকে বেশি কোড ঘাঁটাঘাঁটি করতে হবে। - টেস্ট করা (Testing): Reducer হলো একটি pure function যা আপনার কম্পোনেন্টের উপর নির্ভর করে না। এর মানে আপনি একে আলাদা ভাবে export করে test করতে পারবেন। যদিও স্বাভাবিকভাবে কম্পোনেন্টস কে আরো realistic environment এ টেস্ট করা উত্তম, তবে জটিল state update logic এর ক্ষেত্রে “নির্দিষ্ট initial state এবং action এর জন্য আপনার reducer নির্দিষ্ট state রিটার্ন করে” এ ব্যাপারে নিশ্চিত থাকা উপকারে আসতে পারে।
- ব্যাক্তিগত পছন্দ (Personal preference): কেউ reducer পছন্দ করে, কেউ করেনা। এটা কোনো সমস্যা না। এটা একটা রুচির বিষয়। আপনি সর্বদাই
useState
এবংuseReducer
এর মাঝে অদল বদল করতে পারবেনঃ তারা উভয়ই সমান!
যদি আপনি কোনো কম্পোনেন্টে ভুলভাল স্টেট আপডেটের কারণে bug এর সম্মুখীন হন এবং এর কোডের কাঠামো আরো সুন্দর করতে চান, সেক্ষেত্রে আমরা একটি reducer ব্যাবহার করা রেকমেন্ড করি। আপনার সব কিছুর জন্য reducer ব্যাবহার করতে হবে এমন কোনো কথা নেইঃ আপনি বিনা বাধায় মিলিয়ে মিশিয়ে ব্যাবহার করতে পারেন! এমনকি আপনি একই কম্পোনেন্টে useState
এবং useReducer
ব্যাবহার করতে পারেন।
যেভাবে ভালো reducer লেখবেন
Reducer লেখার সময় এই দুটি টিপস মনে রাখবেনঃ
- Reducer কে অবশই pure হতে হবে। state updater ফাংশনের মতো, reducer সমূহ রেন্ডারের সময় run করে! (Action সমূহকে পরবর্তী রেন্ডার পর্যন্ত সারিবদ্ধ ভাবে দাঁড় করিয়ে রাখা হয়।) এর মানে, reducer সমূহ অবশ্যই pure হতে হবে—একই input একই output দিবে। সেগুলো যেন কোনো request সেন্ড, timeout ঠিক করা, অথবা কোনো সাইড ইফেক্ট (এমন অপারেশন যেটা কম্পোনেন্টের বাইরের কোনো কিছুর উপর প্রভাব ফেলে) পারফর্ম না করে। সেগুলো যেন objects এবং arrays mutations ছাড়াই আপডেট করে।
- প্রতিটি action একটি মাত্র user interaction এর বর্ণনা হবে, যদি তার কারণে ডেটাতে একাধিক পরিবর্তন হয় তবুও। উদাহরণস্বরূপ, যদি একজন ইউজার একটি ফর্মে “Reset” প্রেস করে যে ফর্মের ৫ টি ফিল্ড আছে যেগুলো একটি reducer দ্বারা নিয়ন্ত্রিত, তখন একটি
reset_form
action কে dispatch করাটা পাঁচটি পৃথকset_field
action dispatch করার থেকে যৌক্তিক। আপনি যদি একটি reducer এ প্রতিটি action log করেন, ঐ log গুলো আপনার জন্যও যথেষ্ট বোধগম্য হওয়ার কথা যাতে কি কি ইন্টার্যাকশন বা কি কি রেসপন্স কোনটার পরে কোনটা হয়েছে তা আন্দাজ করতে পারেন। এটা ডিবাগিং এর সময় সাহায্য করে!
Immer দিয়ে সংক্ষেপে reducers লেখা
স্বাভাবিক স্টেটে objects এবং arrays আপডেট করার মতই, আপনি reducer সমূহকে আরো সংক্ষেপ করতে আপনি Immer লাইব্রেরীটি ব্যাবহার করতে পারেন। এখানে, useImmerReducer
আপনাকে 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": {} }
Reducers কে অবশ্যই pure হতে হবে, যেন সেগুলো স্টেটকে mutate না করে। তবে Immer আপনাকে এখানে একটি স্পেশাল draft
অবজেক্ট দিচ্ছে যেটিকে মিউটেট করা সম্পূর্ণ নিরাপদ। চোখের আড়ালে, Immer আপনার স্টেটের একটি কপি তৈরি করে নিবে যার মধ্যে draft
এর মধ্যে আপনি যত কিছু পরিবর্তন করেছেন, সব বিদ্যমান থাকবে। এজন্যে useImmerReducer
দ্বারা নিয়ন্ত্রিত reducers তাদের প্রথম আর্গুমেন্ট মিউটেট করতে পারে এবং তাদের স্টেট রিটার্ন করতে হয়না।
পুনরালোচনা
useState
কেuseReducer
এ পরিবর্তন করতেঃ- ইভেন্ট হ্যান্ডলারসমূহ থেকে actions ডিসপ্যাচ করুন।
- একটি reducer function যেটি প্রদত্ত স্টেটের জন্য পরবর্তী স্টেট রিটার্ন করে এবং action সমূহ লিখুন।
useState
এর জায়গায়useReducer
ব্যবহার করুন।
- Reducers এর জন্য আপনার একটু বাড়তি কোড লিখতে হয়, কিন্তু এরা ডিবাগিং এবং টেস্টিং এ সহায়ক।
- Reducers অবশ্যই pure হতে হবে।
- প্রতিটি action একটি মাত্র user interaction এর বর্ণনা হবে।
- Immer ব্যবহার করুন যদি আপনি reducers কে mutating স্টাইলে লিখতে চান।
চ্যালেঞ্জ 1 / 4: ইভেন্ট হ্যান্ডলারস থেকে actions কে dispatch করুন
এখানে, ContactList.js
এবং Chat.js
এর ইভেন্ট হ্যান্ডলারগুলোতে // TODO
কমেন্ট করা আছে। এজন্যেই ইনপুটটিতে টাইপ করলে কিছু হচ্ছে না, এবং পাশের বাটন গুলোতে ক্লিক করলে মেসেজের প্রাপক বদলাচ্ছেনা।
এই দুইটি // TODO
এর জায়গায় নিজ নিজ action গুলো dispatch
করার কোড লিখুন। action গুলোর কাঙ্ক্ষিত আকৃতি এবং টাইপ জানার জন্য, messengerReducer.js
এর মধ্যের reducer টি দেখুন। Reducer টি অলরেডি লিখে দেয়া হয়েছে, তাই সেটিতে আপনার কোনো পরিবর্তন আনতে হবেনা। আপনার শুধু ContactList.js
এবং Chat.js
এ action গুলো dispatch করতে হবে।
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'}, ];