Reactive Accelerator
React Js
Going Deep into React: Adding Interactivity
2.7 - Updating Objects in a State

রিয়াক্টে যেকোন ধরনের জাভাস্ক্রিপ্ট ভ্যালুকে তার স্টেট হিসেবে নিতে পারে। কিন্তু যখনি আমরা রিয়াক্টের স্টেট হিসেবে কোন জাভাস্ক্রিপ্ট অবজেক্ট ব্যাবহার করবো তা কখনওই আমরা সরাসরি চেঞ্জ করতে পারবোনা বা মিউটেড করতে পারবোনা। এক্ষেত্র আমাদেরকে সম্পূর্ন নতুন অবজেক্ট দিয়ে আগের অবজেক্টকে রিপ্লেস করে দিতে হবে, নাহলে আগের অবজেক্ট থেকে একটা কপি বানিয়ে সেই কপি অবজেক্ট দিয়ে আগের স্টেট এর অবজেক্ট কে রিপ্লেস করে দিতে হবে। অর্থাৎ যেভাবেই করি আমাদেরকে সবসময় স্টেট আপডেট করার সময় নতুন ভ্যালু দিয়ে আগের ভ্যালু রিপ্লেস করে দিতে হবে।

(What is a Mutation) মিউটেশন কি ?

মিউটেশন মানে হলো পরিবর্তন করা । জাভাস্ক্রিপ্টের ভাষায় কোন ভেরিয়েবল এর ভ্যালুকে সরাসরি পরিবর্তন করে ফেলাকে মিউটেশন বলা হয়। রিয়াক্ট স্টেটের ভ্যালুকে সরাসরি পরিবর্তন করাকে নিরুৎসাহিত করে। রিয়াক্টে স্টেট হিসেবে যেকোন ভ্যালু স্টোর করা যায় যেমনঃ string ,number,Boolean,Array ,Object

string ,number,Boolean হলো জাভাস্ক্রিপ্টের প্রিমিটিভ ভ্যালু যা read-only বা কখনো এগুলো পরিবর্তন করা যায়না।

অন্যদিকে Array ,Object হলো রেফারেন্স ভ্যালু। যা টেকনিক্যালি পরিবর্তন করা গেলেও রিয়াক্ট স্টেট-এর ভ্যালুতে সরাসরি রেফারেন্স ভ্যালুকে পরিবর্তন করতে নিষেধ করে। রিয়াক্টে স্টেট আপডেটের ক্ষেত্রে এসব ভ্যালুকেও read-only হিসেবে চিন্তা করতে বলে ।

জাভাস্ক্রিপ্ট এর প্রিমিটিভ ও রেফারেন্স ভ্যালু সম্পর্কে আরও বিস্তারিত জানতে চাইলে এখানে ক্লিক করুন (opens in a new tab)

চলুন একটু উদাহরণের মাধ্যমে বিস্তারিত বুঝিঃ

const [x, setX] = useState(0);

এখানে স্টেট ভ্যারিএবল x এর ভ্যালু হিসেবে 0 রাখা হয়েছে, 0 হলো একটি প্রিমিটিভ ভ্যালু।

setX(5);

এখানে setX করে স্টেট ভ্যারিএবল x এর ভ্যালু 5 করা হয়েছে । এখানে x এর ভ্যালু 0 কে কিন্তু চেঞ্জ করা হয়নি,কেননা এটা হলো একটা প্রিমটিভ ভ্যালু যা কখনো চেঞ্জ করা যায়না বা read-only বরং 5 যা একটি নতুন ভ্যালু, তা দিয়ে আগের ভ্যালুকে রিপ্লেস করা হয়েছে ।

কিন্তু যখন আমাদের স্টেট এর ভ্যালু এমন অবজেক্ট হবে,

const [position, setPosition] = useState({ x: 0, y: 0 });

যদিও এটা টেকনিকালি position.x = 5; এভাবে পরিবর্তন করা সম্ভব কিন্তু এটা করলে তা মেইন অবজেক্ট কে মিউটেড করা হয়। যা রিয়াক্ট কখনওই রিকমেন্ড করেনা। আপনাকে অবশ্যই স্টেট চেঞ্জ করার সময় সম্পুর্ন নতুন ভ্যালু বা নতুন অবজেক্ট দিয়ে আগের ভ্যালুকে রিপ্লেস করে দিতে হবে।

তাহলে কিভাবে করবো চলুন জেনে নেই।

(Treat state as read-only) রিয়াক্টের স্টেটকে read-only চিন্তা করতে হবে।

App.js
import { useState } from "react";
 
export default function MovingDot() {
    const [position, setPosition] = useState({
        x: 0,
        y: 0,
    });
    return (
        <div
            onPointerMove={(e) => {
                position.x = e.clientX;
                position.y = e.clientY;
            }}
            style={{
                position: "relative",
                width: "100vw",
                height: "100vh",
            }}>
            <div
                style={{
                    position: "absolute",
                    backgroundColor: "red",
                    borderRadius: "50%",
                    transform: `translate(${position.x}px, ${position.y}px)`,
                    left: -10,
                    top: -10,
                    width: 20,
                    height: 20,
                }}
            />
        </div>
    );
}

উপরের কোড এ একটা পয়েন্টার বানানো হয়েছে যা মাউস মুভ করার সাথে সাথে মাউসকে ফলো করে মুভ করার কথা। কিন্তু কোডটা রান করলে দেখা যাবে কোডটা কাজ করছেনা।

কাজ করছেনা কারণ কোডের এই অংশে একটু ভুল আছে।

    onPointerMove={e => {
      position.x = e.clientX;
      position.y = e.clientY;
    }}

এখানে onPointerMove event listener এ স্টেট ভ্যারিয়েবল position এর ভ্যালু একবার স্ক্রিনে রেন্ডার হয়ে যাওয়ার পর আবার সেই ভ্যালুকেই সরাসরি পরিবর্তন করা হয়েছে, এজন্যই কাজ করছেনা। কেননা চেঞ্জটা কোন setter function এ করা হচ্ছেনা, তাই রিয়েক্ট জানতেই পারছেনা যে স্টেট এর ভ্যালু চেঞ্জ হয়েছে এবং তাকে রি-রেন্ডার করতে হবে।

এক্ষেত্রে এটা চেঞ্জ করতে হলে setter function এর ভিতরে চেঞ্জ করতে হবে যাতে ভ্যালু চেঞ্জ হলেই রি-রেন্ডার ট্রিগার হয়ে যায়।

onPointerMove((e) => {
    setPosition({
        x: e.clientX,
        y: e.clientY,
    });
});

খেয়াল করে দেখেন, এখানে কিন্তু আগের position অবজেক্ট এর কোন ভ্যালুকে চেঞ্জ করা হয়নি,সম্পুর্ন নতুন একটা অবজেক্ট বানিয়ে আগের অবজেক্ট কে রিপ্লেস করে দেয়া হয়েছে।

📔

লোকাল মিউটেশন হলে সেটা ঠিক আছে।

এভাবে কোড করা যাবেনা,যা স্টেটের বর্তমান অবজেক্ট position কেই সরাসরি চেঞ্জ করে।

position.x = e.clientX;
position.y = e.clientY;

কিন্তু যদি এভাবে কোড করি, যাতে আমরা সম্পুর্ন নতুন একটা অবজেক্ট বানিয়ে আগের স্টেট ভ্যালুর অবজেক্ট কে রিপ্লেস করে দেই,তাহলে সেটা সম্পুর্নভাবে ঠিক আছে।

    setPosition((e)=>{
        x: e.clientX,
        y: e.clientY
    })

আবার একটা লোকাল অবজেক্ট ভ্যারিয়েবল বানিয়ে সেটা দিয়েও চেঞ্জ করতে পারি, যেমনঃ

const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
 
setPosition(nextPosition);

এভাবে করলেও ঠিক আছে কেননা এখানে স্টেট ভ্যারিএবল এর position অবজেক্ট কে মডিফাই করা হচ্ছেনা, এখানে একটা নতুন অবজেক্ট বানিয়ে স্টেট এর আগের ভ্যালুকে রিপ্লেস করা হচ্ছে।

মিউটেশন তখনই প্রব্লেম যখন আপনি কোন স্টেট এর এক্সিস্টীং অবজেক্টে সরাসরি পরিবর্তন করছেন, তা নাহলে আপনি লোকাল ভেরিয়েবল পরিবর্তন করতে পারেন,এতে কোন সমস্যা নাই

Copying objects with the spread syntax

আগের উদাহরণে আমরা যেমন দেখলাম যে যখনি আমরা স্টেট এর অবজেক্ট ভ্যালু পরিবর্তন করবো তখন আমাদের একটা নতুন অবজেক্ট স্টেটে দিতে হবে। তার মানে আমাদের প্রতিবার একটা করে ফ্রেশ অবজেক্ট বানাতে হচ্ছে।

কিন্তু আমাদের অবজেক্ট যদি অনেক বড় হয় তাহলে যখনি স্টেট আপডেট করতে চাইবো তখনি যদি এত বড় অবজেক্ট নতুন করে লিখতে যাই তাহলে সেটা একটা সমস্যা এবং এতে ভুল হওয়ার সম্ভাবনা বেশি থাকে।

এক্ষেত্রে আমরা আগের অবজেক্ট এর একটা কপি বানিয়ে নিয়ে শুধুমাত্র যেই যেই ভ্যালু পরিবর্তন করতে চাই,সেগুলো পরিবর্তন করে দিলেইতো ঝামেলা শেষ।

এই কাজটা করতেই জাভাস্ক্রিপ্ট এর ... spread অপারেটর আমাদের হেল্প করে থাকে। ... spread অপারেটর ব্যাবহার করে আমরা আগের অবজেক্ট এর একটা Shallow কপি বানিয়ে নিতে পারি। তবে মনে রাখতে হবে ... spread অপারেটর নেস্টেড অবজেক্ট এর ক্ষেত্রে শুধুমাত্র এক লেবেল কপি করে। যদি ডিপলি কপি করতে চাই তাহলে ... spread অপারেটর একাধিকবার ব্যাবহার করতে হবে।

Shallow Copy এবং Deep Copy সম্পর্কে আরও বিস্তারিত জানতে এই লিঙ্ক এ ক্লিক করুন (opens in a new tab)

ধরুন আমাদের এই অবজেক্ট স্টেট ভ্যারিয়বলের firstName প্রপার্টির ভ্যালু চেঞ্জ করা লাগবে।

const [person, setPerson] = useState({
    firstName: "Barbara",
    lastName: "Hepworth",
    email: "bhepworth@sculpture.com",
});

তাহলে আমরা এভাবে করতেই পারিঃ

setPerson({
    firstName: "Shahadat Hussain Ripon",
    lastName: "Hepworth",
    email: "bhepworth@sculpture.com",
});

এটা খুব ভালোভাবেই কাজ করবে। কিন্তু এতে আমাদের একই জিনিস বার বার লেখা লাগছে,

কিন্তু যদি আমরা ...spread অপারেটর ব্যাবহার করি তাহলে আমরা আরও সহজে এভাবে লিখতে পারি।

setPerson({
    ...person, // এখানে আমাদের আগের অবজেক্টের সকল ডাটা কপি হয়ে নতুন অবজেক্ট এর প্রপার্টি হিসেবে বসে গেছে।
    firstName: "Shahadat Hussain Ripon", // এখানে আমরা প্রপার্টির ভ্যালু চেঞ্জ করছি
});

(Updating a nested object) কিভাবে নেস্টেড অবজেক্টকে স্টেট এ আপডেট করতে হয়?

ধরুন আমাদের স্টেট এর স্ট্রাকচার এমনঃ

const [person, setPerson] = useState({
    name: "Niki de Saint Phalle",
    artwork: {
        title: "Blue Nana",
        city: "Hamburg",
        image: "https://i.imgur.com/Sd1AgUOm.jpg",
    },
});

এবং আমরা person.artwork.city এর ভ্যালু আপডেট করতে চাই।

তাহলে আমরা এভাবে করতে পারি।

const changedArtwork = { ...person.artwork, city: "Narayangonj" };
const changedPerson = { ...person, artwork: changedArtwork };
setPerson(changedPerson);

অথবা যদি আমরা একটা সিঙ্গেল ফাংশন কলের ভিতরেই চেঞ্জ করতে চাই, তাহলে এভাবে করতে পারি,

setPerson({
    ...person, // আগের অবজেক্ট এর সমস্ত ফিল্ড কপি করা হয়েছে
    artwork: {
        ...person.artwork, // আগের অবজেক্ট এর artwork ফিল্ডের সমস্ত ডাটা কপি করা হয়েছে
        city: "Narayangonj", // প্রপার্টির ভ্যালু পরিবর্তন করা হয়েছে
    },
});
📔

সত্যি হলো যে অবজেক্ট কখনই নেস্টেড নয়।

let obj = {
    name: "Niki de Saint Phalle",
    artwork: {
        title: "Blue Nana",
        city: "Hamburg",
        image: "https://i.imgur.com/Sd1AgUOm.jpg",
    },
};

এই অবজেক্টটাকে এভাবে দেখে মনে হচ্ছে এটা নেস্টেড,কিন্তু সত্যিটা হলো যে যখন আমাদের কোড এক্সিকিউট হবে,তখন আসলে নেস্টেড অবজেক্টের কোন অস্তিত্ব নেই। তখন আসলে এটা দুইটা অবজেক্ট হয়ে যাবে।

let obj1 = {
    title: "Blue Nana",
    city: "Hamburg",
    image: "https://i.imgur.com/Sd1AgUOm.jpg",
};
 
let obj2 = {
    name: "Niki de Saint Phalle",
    artwork: obj1,
};

তারপর হয়তো একটা অবজেক্ট এর রেফারেন্স নিয়ে অন্য আরেকটা অবজেক্ট তৈরি হতে পারে, এভাবেঃ

let obj1 = {
    title: "Blue Nana",
    city: "Hamburg",
    image: "https://i.imgur.com/Sd1AgUOm.jpg",
};
 
let obj2 = {
    name: "Niki de Saint Phalle",
    artwork: obj1,
};
 
let obj3 = {
    name: "Something",
    artwork: obj1,
};

এমন অবস্থায় আপনি যদি obj1 এ কোন চেঞ্জ করেন তাহলে রেফারেন্স ভ্যালু হউয়ার কারনে obj3 তে গিয়েও সেই চেঞ্জটা এফেক্ট করবে।

(Write concise update logic with Immer) ইমার এর মাধ্যমে নিশ্চিন্তে নেস্টেড অবজেক্ট পরিবর্তন করা।

আমরা স্টেট এর নেস্টেড অবজেক্টগুলোকে খুব সহজে ,কোন কিছু চিন্তা ভাবনা ছাড়াই পরিবর্তন করতে চাইলে রিয়াক্ট একটা থার্ড পার্টি প্যাকেজ রিকমেন্ড করে Immerনামে।

আমরা চাইলে Immer ব্যাবহার করেও আমাদের কমপ্লেক্স ন্যাস্টেড অবেজক্ট স্টেট গুলো খুব সহজেই পরিবর্তন করতে পারি।

Immer আমাদেরকে draft নামে একটা স্পেশাল অবজেক্ট দেয়,যেটা আগের কমপ্লেক্স অবজেক্ট এর একটা Proxy অবজেক্ট বানিয়ে নেয়, এবং আমরা কি কি পরিবর্তন করি তা সে খুঁজে বের করে, আগের অবজেক্ট কে ইমিউটিভলি আপডেট করে দেয়। এক্ষেত্রে Mutation এর ব্যাপারটা Immerনিজে ম্যানেজ করে বলে আমাদের টেনশন নিতে হয়না।

Immer কিভাবে ব্যাবহার করতে হয়।

install Immer package

 npm install use-immer

import useImmer

import { useImmer } from "use-immer";

change useState to useImmer

useState কে চেঞ্জ করে useImmer হবে

import { useImmer } from "use-immer";
 
const [person, setPerson] = useState({}); // এই লাইনটিকে চেঞ্জ করতে হবে
const [person, updatePerson] = useImmer({}); // এভাবে স্টেট ডিফাইন করতে হবে।

Update state like below

import { useImmer } from "use-immer";
 
const [person, updatePerson] = useImmer({
    firstName: "Barbara",
    lastName: "Hepworth",
    contact: {
        email: "bhepworth@sculpture.com",
        phone: "+8801913509868",
        github: "deveripon",
    },
});
 
updateImmer((draft) => {
    // state updater immer function
    draft.firstName = "Shahadat Hussain ";
    draft.lastName = "Ripon";
    draft.contact.email = "devripon.io@gmail.com";
});

এভাবে করলে হয়তো মনে হতে পারে আমরা রিয়াক্টের রুলস ব্রেক করছি,কিন্তু না আসলে আমরা এখানে draft কে মিউটেড করলেও, আন্ডার দ্যা হুড, Immer নিজে আমাদের স্টেট টা ইমিউটিভলি আপডেট করছে।