Lecture 59 - Understand Redux, React Redux and EasyPeasy

Lecture 59 - Understand Redux, React Redux and EasyPeasy

আমরা এর আগে রিডাক্স নিয়ে হালকা আলোচনা করেছিলাম। এখন প্রশ্ন হচ্ছে কখন আমরা রিডাক্স ব্যবহার করবো আর কখন রিডাক্স-টুলকিট বা ইজিপিজির মতো র‍্যাপার ব্যবহার করবো। উত্তর হলো যদি আমাদের অ্যাপ্লিকেশন খুব বড় না হয় তখন আমরা র‍্যাপার ব্যবহার করতে পারি। আর যদি আমাদের অ্যাপ্লিকেশন বড় স্কেলের হয় সেক্ষেত্রে কোনো র‍্যাপার ব্যবহার না করে পিওর রিডাক্স ব্যবহার করাই বুদ্ধিমানের কাজ। কারণ যখন আমরা কোনো বড় অ্যাপ্লিকেশন বানাবো, যেটা অনেক লম্বা সময় ধরে চলবে সেক্ষেত্রে সকল ফ্লেক্সিবিলিটি আমাদের হাতে রেখে দেয়াই ভাল।

এখন রিডাক্স নিয়ে কাজ করতে গেলে আমাদের আগে বুঝতে হবে রিডাক্স আমাদের জন্য কি করে। আমরা রিয়্যাক্টের useState ব্যবহার করে যা করি, ঠিক সেই কাজটাই রিডাক্স আমাদের জন্য করে। আমরা useState ব্যবহার করি কম্পোনেন্টে। রিডাক্সের ক্ষেত্রে আমরা সেই স্টেট বের করে অন্য কোথাও রাখবো যেখান থেকে সবাই এই স্টেট ব্যবহার করতে পারবে। সেই সাথে ডাটা আপডেট, ডিলিট ইত্যাদি করতে পারবে। কিন্তু রিডাক্সের ক্ষেত্রে সেটআপটা একটু বড়। চলুন আমরা সরাসরি কোডে চলে যাই।

Installation of redux

আমরা প্রথমে Redux ইনস্টল করে নিবো নিচের কমান্ড ব্যবহার করে।

yarn add redux

Redux Setup

রিডাক্স নিয়ে কাজ করতে গেলে আমাদের প্রথমে সেটাকে ইমপোর্ট করতে হবে। আমরা আমাদের main.jsx ফাইলে গিয়ে রিডাক্স থেকে createStore ইমপোর্ট করে নিবো। বলে রাখা ভাল, এটা অলরেডি Deprecated. জাস্ট বুঝানোর জন্য এটা ব্যবহার করা হচ্ছে। এবার আমরা একটা স্টোর ক্রিয়েট করবো। স্টোরে আমরা যেকোনো ডাটা স্টোর করে রাখতে পারি। কিন্তু ডাটা আপডেট করে রাখতে পারি না। আপডেট করার জন্য আমাদের দরকার রিডিউসার। আবার সরাসরি আপডেট করেতে পারি না। আপডেট করার জন্য আমাদের ডিসপ্যাচ করতে হয়। ডিসপ্যাচ করার অর্থ হচ্ছে একটা ম্যাসেজ পাস করতে হয়। এই কাজ করতে গেলে আমাদের দরকার একটা অ্যাকশন। অ্যাকশনের আবার অনেকগুলো টাইপ থাকতে পারে। অ্যাকশন টাইপ নিয়ে পরবর্তীতে বিস্তারিত আলোচনা করা হবে। ধরে নিলাম আমাদের এখানে দুইটা অ্যাকশন টাইপ থাকবে। INCREMENT এবং DECREMENT । এগুলোর উপর ভিত্তি করে আমাদের অ্যাকশন তৈরি করতে হবে। অ্যাকশন বলতে বুঝায় সিম্পল একটা অবজেক্ট। যেমন { type: INCREMENT } একটা অ্যাকশন।

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';

import { createStore } from 'redux';

// action type
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// action
{
    type: INCREMENT;
}

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

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

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';

import { createStore } from 'redux';

// action type
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// action creator
const increment = (payload) => ({
    type: INCREMENT,
    payload,
});

dispatch(increment({ payload: 1 }));

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

এখানে অ্যাকশন ক্রিয়েটর বলতে বুঝানো হচ্ছে একটি ফাংশন যার প্যারামিটার হিসেবে আমরা নিলাম payloadpayload নাম্বার, স্ট্রিং, অবজেক্ট বা অ্যারে যেকোনো কিছু হতে পারে। এই ফাংশন থেকে আমরা একটা অবজেক্ট রিটার্ন করে দিলাম যেখানে টাইপ আর payload থাকবে। এই অ্যাকশন আমরা ডিসপ্যাচ করতে পারি dispatch(increment({ payload: 1 })) এর মাধ্যমে। এখানে payload হিসেবে আমরা একটা অবজেক্ট পাস করলাম যার ভ্যালু দিলাম 1 । অর্থাৎ এক করে বৃদ্ধি পাবে। আর যদি Decrement হয় সেক্ষেত্রে আমরা payload হিসেবে পাস কর -1, অর্থাৎ এক করে হ্রাস পাবে।

দেখেন এতক্ষণ পর্যন্ত আমরা action, action type এবং action creator নিয়ে পড়ে আছি। কিন্তু এগুলোর বেসিক্যালি এখন কোনো ভ্যালু নাই যতক্ষণ না পর্যন্ত আমার স্টোর এবং রিডিউসার রেডি হচ্ছে। স্টোর বলতে বুঝায় সমস্ত ডাটা। রিডাক্সের পূর্বে ছিল ফ্লাক্স। ফেসবুকের যে ইন্টারফেসটা দেখা যায় তাতে পূর্বে এবং খুব সম্ভবত এখনও ফ্লাক্স ব্যবহার করা হয়। ফ্লাক্স এবং রিডাক্সের মধ্যে ছোট একটা পার্থক্য আছে। ফ্লাক্সের ক্ষেত্রে ব্যাপারটা অনেকটা কনটেক্সট এপিআইয়ের মতো। অর্থাৎ যতগুলো entity বা entity type থাকবে প্রতিটার জন্য আলাদা আলাদা স্টোর থাকবে। কিন্তু রিডাক্সের ক্ষেত্রে হচ্ছে সব এক জায়গায় থাকবে। এখন অনেক প্রশ্ন করতে পারেন, একসাথে এক জায়গায় রাখলে কি বেশি জায়গা খরচ হচ্ছে না বা আলাদা আলাদা জায়গায় রাখলে কি বেশি এফিশিয়েন্ট হচ্ছে না? উত্তর হচ্ছে না। দিন শেষে আমার সব ডাটা র‍্যামেই থাকছে, সুতরাং যা জায়গা লাগার তাই লাগছে। কোনো ডিফারেন্স নেই। আপনি আলাদা আলাদা ভাবে স্টোর বানান বা এক জায়গায় স্টোর বানান কথা একই।

রিডাক্স কাজ করে সবগুলো ডাটা এক জায়গায় রাখার মাধ্যমে। এক জায়গায় কিভাবে রাখে? রিডিউসারের মাধ্যমে। সেটা কিভাবে। ধরেন আপনার অ্যাপ্লিকেশনে ইউজার, প্রোডাক্ট, অর্ডার ইত্যাদি আরো অনেক ডাটা রয়েছে। আমরা প্রত্যেকটার জন্য একটা করে রিডিউসার বানাবো। এরপর রিডাক্সের combineReducers নামের একটা সিস্টেম আছে, সেটা কল করে দিবো। ব্যস, হয়ে গেলো। সে সবগুলো রিডিউসার থেকে একটা রিডিউসারে কনভার্ট করে আপনাকে রিটার্ন করে দিবে। এখন রিডিউসার কেমন হয়? রিয়্যাক্টে useReducer ব্যবহার করার জন্য যে রিডিউসার ফাংশনটা লাগে সেই রিডিউসার ফাংশনটা যেমন হয়, রিডাক্সের রিডিউসারটাও ঠিক তেমনই হয়। অর্থাৎ

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'

import { createStore } from 'redux';

// action type
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// action creator
const increment = (payload) => ({
    type: INCREMENT,
    payload,
});

const countReducer = (state, action) => {}

const store = createStore(countReducer);

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

এখানে countReducer একটি রিডিউসার ফাংশন যার দুইটা প্যারামিটার থাকে, state আর action. এখন এই রিডিউসারকে createStore এর মধ্যে পাস করে দিবো। আমাদের স্টোর রেডি। এখন যদি আমরা স্টোরকে কনসোলে লগ করি তাহলে আমরা আমাদের স্টোর দেখতে পাবো।

ব্রাউজারে গেলে আমরা একটা অবজেক্ট দেখতে পাবো কনসোলে যেখানে dispatch, getState ইত্যাদি অনেক প্রোপার্টিজ দেখতে পাবো। মূল কথা হচ্ছে আমরা স্টোর বানাতে পেরেছি। এখন যদি আমরা console.log(store.getState()) লিখি তাহলে ব্রাউজার কনসোলে দেখবো undefined শো করছে। কারণ আমরা আমাদের রিডিউসারে state ডিফাইন করে দিইনি। যদি আমরা আমাদের স্টেট হিসেবে নিই একটা অ্যারে -

const countReducer = (state = [], action) => {
    return state;
};

তাহলে সে দেখাবে একটা empty array.

এরকম যদি আমি ডিফাইন করি অবজেক্ট স্টোর রিটার্ন করবে একটা অবজেক্ট। যদি আমি ডিফাইন করি একটা বুলিয়ান স্টোর রিটার্ন করে একটা বুলিয়ান। বেসিক্যালি এই জায়গায় কাউন্টের স্টেট হবে একটা নাম্বার।

এখন ধরেন আমাদের আরেকটা রিডিউসার বানালাম হিস্টোরি রাখার জন্য।

const historyReducer = (state = [], action) => {
  return state;
}

এখন তো আমাদের কাছে দুইটা রিডিউসার। একটা countReducer, অন্যটা historyReducer। এখন আমরা কিভাবে করবো। আমরা জাস্ট রিডাক্সের combineReducers ইমপোর্ট করবো। এবং এই দুইটা রিডিউসার কম্বাইন করে ফেলবো নিচের কোডের মতো।

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';

import { combineReducers, createStore } from 'redux';

// action type
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// action creator
const increment = (payload) => ({
    type: INCREMENT,
    payload,
});

const countReducer = (state = 0, action) => {
    return state;
};

const historyReducer = (state = [], action) => {
    return state;
};

const store = createStore(
    combineReducers({ count: countReducer, history: historyReducer })
);
console.log(store.getState());

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

এখন যদি আমরা ব্রাউজার কনসোলে যায় একটা অবজেক্ট দেখবো {count: 0, history: Array(0)}। তার মানে আমি দুইটা আলাদা আলাদা ভাবে তৈরি করছি, কিন্তু দুইটা একই জায়গায় সুন্দরভাবে ম্যানেজ করতে পারছি।

আমরা যে রিডিউসার বানালাম এগুলো হলো রিডাক্সের জান প্রাণ। কারণ আল্টিমেটলি স্টেট আপডেট করার দায়িত্ব হচ্ছে এই রিডিউসারের। এই রিডিউসার ফাংশন যা রিটার্ন করবে সেটাই হচ্ছে আমাদের নতুন স্টেট। আর্গুমেন্ট আকারে যে স্টেটটা পাবে সেটা হচ্ছে পূর্বের স্টেট। তার মানে আর্গুমেন্ট আকারে যা থাকছে সেটা পূর্বের স্টেট, যা রিটার্ন করছে সেটা নতুন স্টেট। আর এর মাঝে আমাদের যা যা করতে হয় তা করবো।

const countReducer = (/* previous state */state = 0, action) => {
  // processing area

    return state; // next state
};

ডাটা পাওয়ার পর আমাদের কাজ হলো কি কি অ্যাকশন হতে পারে তা বের করা। যেমন countReducer এর ক্ষেত্রে অ্যাকশন হিসেবে আমরা নিয়েছিলাম INCREMENT এবং DECREMENT। আরো অনেক ধরণের অ্যাকশন থাকতে পারে। যেমন কোনো প্রোডাক্টের ক্ষেত্রে fetch product, add product, create product, update product, delete product ইত্যাদি প্রতিটি কাজই এক একটি অ্যাকশন। অর্থাৎ আমরা কি করতে চাইছি, সেটাই হচ্ছে অ্যাকশন।

আমার স্টেট পরিবর্তন কিভাবে হবে সেটা নির্ভর করে অ্যাকশন টাইপের উপর। এই উদাহরণে যদি অ্যাকশন টাইপ INCREMENT হয় তবে স্টেটের ভ্যালু বৃদ্ধি পাবে আর যদি অ্যাকশন টাইপ DECREMENT হয় তবে স্টেটের ভ্যালু হ্রাস পাবে। এই কাজ switch স্টেটমেন্টের মাধ্যমে খুব সহজেই করা যায়। যেমনঃ

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';

import { combineReducers, createStore } from 'redux';

// action type
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// action creator
const increment = (payload) => ({
    type: INCREMENT,
    payload,
});

const countReducer = (/* previous state */ state = 0, action) => {
    // processing area
    switch (action.type) {
        case INCREMENT:
            return state + action.payload;
        case DECREMENT:
            return state - action.payload;
        default:
            return state;
    }
};

const historyReducer = (state = [], action) => {
    return state;
};

const store = createStore(
    combineReducers({ count: countReducer, history: historyReducer })
);
console.log(store.getState());

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

আমরা countReducer ফাংশনের মধ্যে দেখেন একটা সুইচ স্টেটমেন্ট নিলাম। যার ভ্যালু হিসেবে নিবো action.type । প্রথমে ক্ষেতে কেইস যখন ইনক্রিমেন্ট তখন স্টেটের সাথে action.payload যোগ হচ্ছে। এখন payload যদি ১ হয় ১ যোগ হবে, যদি ১০০ হয় ১০০ যোগ হবে। এরপর কেইস যদি হয় ডিক্রিমেন্ট তখন স্টেট থেকে action.payload বিয়োগ হচ্ছে। আর default হিসেবে স্টেট যেমন আছে তেমনই রিটার্ন করবে। ধরেন আমাদের এখানে গুণ করার কোনো অ্যাকশন নাই। যদি ইউজার গুণ করার অ্যাকশন ডিসপ্যাচ করে তাহলে আমরা তাকে আমাদের স্টেটে হাতই দিতে দিবো না। সরাসরি স্টেট যেমন আছে তেমনই রিটার্ন করে দিবো।

এখন এখানে ডিক্রিমেন্ট করার কোনো অ্যাকশন বানানো হয়নি। আমরা একটা অ্যাকশন বানিয়ে নিতে পারি।

const decrement = (payload) => ({
    type: DECREMENT,
    payload,
});

এবার আমাদের কাছে আছে historyReducer । এর জন্য কি কি অ্যাকশন হতে পারে। চলুন দেখি।

const ADD_TO_HISTORY = 'ADD_TO_HISTORY';
const CLEAR_HISTORY = 'CLEAR_HISTORY';

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

const historyReducer = (state = [], action) => {
    switch (action.type) {
    case ADD_TO_HISTORY:
      // return state.push(action.payload) // ❌
      return [...state, action.payload]; // ✅
    case CLEAR_HISTORY:
      return [];
    default:
      return state;
  }
};

অর্থাৎ স্টেট মিউটেট করা যাবে না। সবসময় নতুন স্টেট, এক্ষেত্রে নতুন অ্যারে, রিটার্ন করতে হবে।

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

let id = 1;
function generateID() {
    return id++;
}

const addHistory = (history) => ({
    type: ADD_TO_HISTORY,
    payload: {
        id: generateID(),
        action: history.action,
        count: history.count,
        time: new Date(),
    },
});

আমরা এখানে টাইপ হিসেবে নিলাম ADD_TO_HISTORY, আর payload হিসেবে একটা অবজেক্ট নিলাম। সেই অবজেক্টে একটা আইডি রাখলাম, অ্যাকশন, কাউন্ট এবং টাইম রাখলাম। এবার আমরা হিস্টোরি ক্লিয়ার করার জন্য অ্যাকশন ক্রিয়েটর বানাবো। আপনার রিডিউসার ফাংশন লক্ষ্য করলে দেখবেন আমাদের হিস্টোরি ক্লিয়ার করতে কোনো ধরণের payload প্রয়োজন হচ্ছে না। সুতরাং আমাদের অ্যাকশন ক্রিয়েটরের মধ্যেও কোনো payload রাখার দরকার নেই। চলুন তাহলে অ্যাকশন ক্রিয়েটরটি বানিয়ে ফেলা যাক।

const clearHistory = () => ({
    type: CLEAR_HISTORY,
});

মোটামুটি কাজ করার জন্য যা যা দরকার তা তা হয়ে গেছে। আমি যদি পুরো কোডটা আরেকবার এখানে লিখি তাহলে দেখতে এমন দেখাবে।

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';

import { combineReducers, createStore } from 'redux';

// action type
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// action creator
const increment = (payload) => ({
    type: INCREMENT,
    payload,
});

const decrement = (payload) => ({
    type: DECREMENT,
    payload,
});

const countReducer = (/* previous state */ state = 0, action) => {
    // processing area
    switch (action.type) {
        case INCREMENT:
            return state + action.payload;
        case DECREMENT:
            return state - action.payload;
        default:
            return state;
    }
};

const ADD_TO_HISTORY = 'ADD_TO_HISTORY';
const CLEAR_HISTORY = 'CLEAR_HISTORY';

let id = 1;
function generateID() {
    return id++;
}

const addHistory = (history) => ({
    type: ADD_TO_HISTORY,
    payload: {
        id: generateID(),
        action: history.action,
        count: history.count,
        time: new Date(),
    },
});

const clearHistory = () => ({
    type: CLEAR_HISTORY,
});

const historyReducer = (state = [], action) => {
    switch (action.type) {
        case ADD_TO_HISTORY:
            return [...state, action.payload];
        case CLEAR_HISTORY:
            return [];
        default:
            return state;
    }
};

const store = createStore(
    combineReducers({ count: countReducer, history: historyReducer })
);

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

এবার আমাদের স্টেট আপডেট কিভাবে হচ্ছে তা দেখবো। আমরা প্রথমে store.getState() কনসোলে লগ করলে {count: 0, history: Array(0)} অবজেক্টটা পেয়েছিলাম। এবার আমরা যদি নিচের কোডের মতো স্টোর থেকে ডিসপ্যাচ করি তাহলে কি হয় একটু দেখি।

console.log(store.getState());
store.dispatch(increment(1));
store.dispatch(addHistory({ action: 'INCREMENT', count: 1 }));
store.dispatch(increment(5));
store.dispatch(addHistory({ action: 'INCREMENT', count: 5 }));
console.log(store.getState());

এখানে প্রথমবার increment এর payload হিসেবে ১ দিলাম এবং পরেরবার ৫ দিলাম। দুইটা চেইঞ্জকেই হিস্টোরিতে অ্যাড করলাম। এবার ডিসপ্যাচ করার পর স্টোরের স্টেটের অবস্থা দেখার চেষ্টা করি আমরা।

দেখেন কত সুন্দর করে আপডেট হয়ে গেলো।

এই যে ছোট ছোট কাজগুলো করলাম এই ছোট ছোট কাজগুলো আমরা রিয়্যাক্টে ইমপ্যাক্ট ফেলতে পারবো। এখন সেই ইমপ্যাক্ট ফেলতে হলে আমাদের ছোট একটা টুল প্রয়োজন। সেই টুলটা হলো react-redux. আমরা এটা ইনস্টল করে নিবো yarn add react-redux এই কমান্ডের মাধ্যমে।

এবার আমরা react-redux থেকে Provider ইমপোর্ট করে নিবো। এবং নিচের মতো করে App কে Provider দিয়ে র‍্যাপ করে দিবো।

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';

import { Provider } from 'react-redux';
import { combineReducers, createStore } from 'redux';

// ... Previous codes

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <Provider store={store}>
            <App />
        </Provider>
    </React.StrictMode>
);

মনে রাখবেন যদি Provider এর মধ্যে স্টোর পাস না করেন তবে সেটা কাজ করবে না। এখন আমরা চাইলে অ্যাপ্লিকেশনের যেকোনো জায়গা থেকে এই ডাটাগুলো ব্যবহার করতে পারি। কিন্তু এখনই পারবো না, কারণ আমরা এখানে যে যে কাজগুলো করেছি সেগুলো অন্য একটা ফাইলে রাখতে হবে। কারণ, অ্যাপ্লিকেশনের বিভিন্ন জায়গা থেকে আমাদের এগুলো ইমপোর্ট করতে হবে। যেহেতু আমরা এগুলো মেইন ফাইলের মধ্যে রেখেছি, মেইন ফাইল থেকে কিছু যখন আমাদের অ্যাপ্লিকেশনের অন্য জায়গায় ব্যবহার করতে যাবো তখন একটা সার্কুলার ডিপেন্ডেন্সি ক্রিয়েট হবে। তাই আমরা নতুন একটা ফাইল ক্রিয়েট করবো src এর মধ্যেই যার নাম আমরা দিলাম store.js। এবার মেইন ফাইলে থাকে নিচের কোডগুলো কপি করে আমরা নতুন ফাইলে রাখবো। এবং অ্যাকশন ও অ্যাকশন ক্রিয়েটরের আগে export কীওয়ার্ডটা বসাবো। আর আমাদের store কে default ভাবে এক্সপোর্ট করে দিবো।

// store.js

import { combineReducers, createStore } from 'redux';

// action type
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';

// action creator
export const increment = (payload) => ({
    type: INCREMENT,
    payload,
});

export const decrement = (payload) => ({
    type: DECREMENT,
    payload,
});

const countReducer = (/* previous state */ state = 0, action) => {
    // processing area
    switch (action.type) {
        case INCREMENT:
            return state + action.payload;
        case DECREMENT:
            return state - action.payload;
        default:
            return state;
    }
};

export const ADD_TO_HISTORY = 'ADD_TO_HISTORY';
export const CLEAR_HISTORY = 'CLEAR_HISTORY';

let id = 1;
function generateID() {
    return id++;
}

export const addHistory = (history) => ({
    type: ADD_TO_HISTORY,
    payload: {
        id: generateID(),
        action: history.action,
        count: history.count,
        time: new Date(),
    },
});

export const clearHistory = () => ({
    type: CLEAR_HISTORY,
});

const historyReducer = (state = [], action) => {
    switch (action.type) {
        case ADD_TO_HISTORY:
            return [...state, action.payload];
        case CLEAR_HISTORY:
            return [];
        default:
            return state;
    }
};

const store = createStore(
    combineReducers({ count: countReducer, history: historyReducer })
);

export default store;

এবার আমরা মেইন ফাইলে store.js থেকে store ইমপোর্ট করে নিবো।

// main.jsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';

import { Provider } from 'react-redux';
import store from './store.js';

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <Provider store={store}>
            <App />
        </Provider>
    </React.StrictMode>
);

আমরা একটা কম্পোনেন্ট বানাবো যার নাম দিলাম Count.jsx। এর মধ্যে যদি আমরা আমাদের স্টোর থেকে স্টেট নিয়ে আসতে চাই তাহলে আমাদের দরকার পড়বে react-redux এর useSelector হুকটি। সেটা কিভাবে করবো চলুন আমরা দেখি।

// components/Count.jsx

import { useSelector } from 'react-redux';

const Count = () => {
    const state = useSelector((state) => state);
    console.log(state);

    return <div>Count</div>;
};

export default Count;

আমরা দেখতে চাইছি স্টেটের মধ্যে কি কি থাকে। তবে তার জন্য আমাদের এই কম্পোনেন্টকে অ্যাপের মধ্যে ইমপোর্ট করতে হবে আগে।

// App.jsx

import Count from './components/Count';

function App() {
    return (
        <div>
            <Count />
        </div>
    );
}

export default App;

এবার ব্রাউজারে গেলে কনসোলে খুললেই দেখবো -

দেখেন আমরা স্টেটের মধ্যে count এবং history দুইটাই পেয়েছি। ওয়ার্নিংটা ইগ্নোর করতে পারেন। কিন্তু আমাদের কাউন্ট কম্পোনেন্টের মধ্যে হিস্টোরির কোনো প্রয়োজন নেই। যেহেতু প্রয়োজন নেই সেহেতু আমরা শুধু কাউন্টকেই রিটার্ন করবো।

// components/Count.jsx

import { useSelector } from 'react-redux';

const Count = () => {
    const count = useSelector((state) => state.count);

    return (
        <div>
            <h1>Counter: {count}</h1>
        </div>
    );
};

export default Count;

এবার যদি ব্রাউজারে দেখি দেখবো নিচের ছবি।

কাউন্টার রিডিউসারে আমরা ইনিশিয়াল স্টেট দিয়েছিলাম 0 তাই এখানে 0 দেখাচ্ছে। কাউন্ট কম্পোনেন্টের কাজ এখানে শেষ। এবার আমরা বানাবো IncrementBtn.jsx নামের কম্পোনেন্ট।

// components/IncrementBtn.jsx

const IncrementBtn = () => {
    return <button>+</button>;
};

export default IncrementBtn;

এবার এটাকে অ্যাপের মধ্যে ইমপোর্ট করবো।

// App.jsx

import Count from './components/Count';
import IncrementBtn from './components/IncrementBtn';

function App() {
    return (
        <div>
            <Count />
            <div>
                <IncrementBtn />
            </div>
        </div>
    );
}

export default App;

এবার ব্রাউজারে গেলে আমরা দেখবো একটা বাটন অ্যাড হয়ে গেছে।

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

import { increment } from '../store';

const IncrementBtn = () => {
    return <button onClick={() => increment(1)}>+</button>;
};

export default IncrementBtn;

কিন্তু এখনও এটি কোনো কাজ করবে না। কারণ এখান থেকে স্টেটের কাছে কোনো ম্যাসেজ যাচ্ছে না। এই ফাংশন একটা অবজেক্ট রিটার্ন করছে যা হচ্ছে অ্যাকশন। এখন অ্যাকশন ঘটাতে হলে আমাদের ডিসপ্যাচ করতে হবে। ডিসপ্যাচ করার জন্য আমাদের দরকার একটা হুক। সেই হুকটি হলো useDispatch। এটি আমরা পাবো react-redux থেকে।

import { useDispatch } from 'react-redux';
import { increment } from '../store';

const IncrementBtn = () => {
    const dispatch = useDispatch();

    return <button onClick={() => dispatch(increment(1))}>+</button>;
};

export default IncrementBtn;

এখন ব্রাউজারে গিয়ে আমরা + বাটনে ক্লিক করলেই দেখবো কাউন্টার চেইঞ্জ হচ্ছে অর্থাৎ স্টেট আপডেট হচ্ছে।

এখন একইভাবে আমরা ডিক্রিমেন্ট কম্পোনেন্টও বানিয়ে ফেলি। আমরা একটা ফাইল ক্রিয়েট করবো যার নাম DecrementBtn.jsx

import { useDispatch } from 'react-redux';
import { decrement } from '../store';

const DecrementBtn = () => {
    const dispatch = useDispatch();

    return <button onClick={() => dispatch(decrement(1))}>-</button>;
};

export default DecrementBtn;

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

import Count from './components/Count';
import DecrementBtn from './components/DecrementBtn';
import IncrementBtn from './components/IncrementBtn';

function App() {
    return (
        <div>
            <Count />
            <div>
                <IncrementBtn />
                <DecrementBtn />
            </div>
        </div>
    );
}

export default App;

এখন ব্রাউজারে গেলে দেখবো এই চেহারা।

এখন + বাটনে ক্লিক করলে কাউন্ট বৃদ্ধি পেতে থাকবে আর - বাটনে ক্লিক করলে হ্রাস পেতে থাকবে।

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

import { useDispatch } from 'react-redux';
import { INCREMENT, addHistory, increment } from '../store';

const IncrementBtn = () => {
    const dispatch = useDispatch();

    const handleClick = () => {
        dispatch(increment(1));
        dispatch(addHistory({ action: INCREMENT, count: 1 }));
    };

    return <button onClick={handleClick}>+</button>;
};

export default IncrementBtn;

এবার হিস্টোরি অ্যাড হচ্ছে কিনা সেটা আমরা আমাদের অ্যাপ ফাইল থেকে দেখার চেষ্টা করি।

import { useSelector } from 'react-redux';

import Count from './components/Count';
import DecrementBtn from './components/DecrementBtn';
import IncrementBtn from './components/IncrementBtn';

function App() {
    const state = useSelector((state) => state);
    console.log(state);

    return (
        <div>
            <Count />
            <div>
                <IncrementBtn />
                <DecrementBtn />
            </div>
        </div>
    );
}

export default App;

এখন ব্রাউজারে গেলে দেখবো ইনক্রিমেন্ট হওয়ার সাথে সাথে হিস্টোরিও আপডেট হচ্ছে।

এখন আমরা ডিক্রিমেন্টের জন্যও সেইম কাজটা করে ফেলি।

import { useDispatch } from 'react-redux';
import { DECREMENT, addHistory, decrement } from '../store';

const DecrementBtn = () => {
    const dispatch = useDispatch();

    const handleClick = () => {
        dispatch(decrement(1));
        dispatch(addHistory({ action: DECREMENT, count: 1 }));
    };

    return <button onClick={handleClick}>-</button>;
};

export default DecrementBtn;

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

এবার আমরা চাইছি হিস্টোরিকে কোনো একটা কম্পোনেন্টে শো করানোর জন্য। সেটার জন্য আমরা History.jsx নামে একটা কম্পোনেন্ট নিলাম।

import { useDispatch, useSelector } from 'react-redux';
import { clearHistory } from '../store';

const History = () => {
    const history = useSelector((state) => state.history);
    const dispatch = useDispatch();

    return (
        <div>
            <p>
                Histories{' '}
                <button onClick={() => dispatch(clearHistory())}>Clear History</button>
            </p>
            <ul>
                {history &&
                    history.map((item) => (
                        <li key={item.id}>
                            {' '}
                            {item.action} - {item.count} - {item.time.toISOString()}{' '}
                        </li>
                    ))}
            </ul>
        </div>
    );
};

export default History;

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

import { useSelector } from 'react-redux';

import Count from './components/Count';
import DecrementBtn from './components/DecrementBtn';
import History from './components/History';
import IncrementBtn from './components/IncrementBtn';

function App() {
    const state = useSelector((state) => state);
    console.log(state);

    return (
        <div>
            <Count />
            <div>
                <IncrementBtn />
                <DecrementBtn />
            </div>
            <History />
        </div>
    );
}

export default App;

ব্রাউজারে যদি যাই দেখবো প্রতি ক্লিকে হিস্টোরি শো করছে।

এবং Clear History বাটনে ক্লিক করলে হিস্টোরি ক্লিয়ার হয়ে যাচ্ছে।

আশা করি রিডাক্স সম্পর্কে ধারণা একটু হলেও ক্লিয়ার হয়েছে। এতক্ষণ আমরা একদম raw redux নিয়ে কাজ করেছি।

এখন আমরা যে স্টোরটি বানিয়েছি সেই স্টোরটিই বানাবো easy-peasy ব্যবহার করে। redux এ আমরা বানাতাম reducer আর এখানে আমরা বানাবো model. model হলো সিম্পলি একটা অবজেক্ট। আমরা আমাদের স্টোর ফাইলকে একটু আপডেট করি easy-peasy দিয়ে।

import { createStore } from 'easy-peasy';

const store = createStore({});

export default store;

সেই সাথে আমাদের Provider ও চেইঞ্জ করতে হবে মেইন ফাইলে। আমরা Provider এর পরিবর্তে ব্যবহার করবো easy-peasy এর StoreProvider.

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';

import { StoreProvider } from 'easy-peasy';
import store from './store.js';

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <StoreProvider store={store}>
            <App />
        </StoreProvider>
    </React.StrictMode>
);

এবার আমাদের দরকার কিছু মডেল। মডেল হচ্ছে একটি অবজেক্ট। এখন আমরা একটা কাউন্টার মডেল তৈরি করবো।

import { action, createStore } from 'easy-peasy';

const counterModel = {
    value: 0,
    increment: action((state, payload) => (state.value += payload)),
    decrement: action((state, payload) => (state.value -= payload)),
};

const store = createStore({
    count: counterModel,
});

export default store;

এখানে মডেলের প্রথম প্রোপার্টি হলো অ্যাকচুয়াল স্টেট। এখন আমাদের ইনক্রিমেন্ট ও ডিক্রিমেন্ট করতে হবে। এগুলো হচ্ছে এক একটা অ্যাকশন। সেগুলো আমরা ডিফাইন করার জন্য easy-peasy থেকে action ইমপোর্ট করে নিয়ে আসবো। এটি আর্গুমেন্ট আকারে নেয় একটি ফাংশন, যার আর্গুমেন্ট আকারে যাবে state এবং payload. এরপর আমরা আমাদের স্টোরে অবজেক্ট আকারে এই মডেলটি দিয়ে দিবো।

Redux এ প্রথমে আমাদের অ্যাকশন টাইপ বানাতে হয়েছিল। এরপর অ্যাকশন ক্রিয়েটর, এরপর রিডিউসার। এখানে এই সব কাজ আমরা জাস্ট তিন লাইনের একটা মডেলের মাধ্যমে করতে পারছি।

এবার কাউন্ট কম্পোনেন্টে গিয়ে আমাদের স্টোর থেকে কাউন্টের ভ্যালুটা নিয়ে নিবো।

import { useStoreState } from 'easy-peasy';

const Count = () => {
    const count = useStoreState((state) => state.count.value);

    return (
        <div>
            <h1>Counter: {count}</h1>
        </div>
    );
};

export default Count;

এখানে useSelector এর জায়গায় ইউজ করেছি easy-peasy এর useStoreState হুক। স্টেট হলো একটা অবজেক্ট যার মধ্যে আছে কাউন্ট নামে মডেল বা অবজেক্ট। এবং কাউন্ট থেকে আমাদের দরকার value । সেটাই আমরা নিয়ে আসলাম। এখন আমরা যদি ব্রাউজারে গিয়ে আমাদের রিডাক্স ডেভ টুল ওপেন করি দেখবো আমাদের স্টেট আপডেট হয়ে গেছে।

এখন আমাদের একটা সিস্টেম খুব সুন্দরভাবে কাজ করছে। এবার আমরা ইনক্রিমেন্ট এবং ডিক্রিমেন্ট নিয়ে কাজ করবো।

// IncrementBtn.jsx

import { useStoreActions } from 'easy-peasy';

const IncrementBtn = () => {
    const { count } = useStoreActions((actions) => actions);

    const handleClick = () => {
        count.increment(1);
    };

    return <button onClick={handleClick}>+</button>;
};

export default IncrementBtn;
// DecrementBtn.jsx

import { useStoreActions } from 'easy-peasy';

const DecrementBtn = () => {
    const { count } = useStoreActions((actions) => actions);

    const handleClick = () => {
        count.decrement(1);
    };

    return <button onClick={handleClick}>-</button>;
};

export default DecrementBtn;

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

এবার হিস্টোরি মডেল নিয়ে কাজ করা যাক।

// store.js

import { action, createStore } from 'easy-peasy';

const counterModel = {
    value: 0,
    increment: action((state, payload) => (state.value += payload)),
    decrement: action((state, payload) => (state.value -= payload)),
};

const historyModel = {
    items: [],
    addHistory: action((state, payload) => {
        state.items.push({
            id: generateID(),
            action: payload.action,
            count: payload.count,
            time: new Date(),
        });
    }),
    clearHistory: action((state) => (state.items = [])),
};

const store = createStore({
    count: counterModel,
    history: historyModel,
});

let id = 1;
function generateID() {
    return id++;
}

export default store;

এবার IncrementBtn এবং DecrementBtn এ হিস্টোরি আপডেট করার প্রসেসিং করবো।

// IncrementBtn.jsx

import { useStoreActions } from 'easy-peasy';

const IncrementBtn = () => {
    const { count, history } = useStoreActions((actions) => actions);

    const handleClick = () => {
        count.increment(1);
        history.addHistory({ action: 'increment', count: 1 });
    };

    return <button onClick={handleClick}>+</button>;
};

export default IncrementBtn;
// DecrementBtn.jsx

import { useStoreActions } from 'easy-peasy';

const DecrementBtn = () => {
    const { count, history } = useStoreActions((actions) => actions);

    const handleClick = () => {
        count.decrement(1);
        history.addHistory({ action: 'decrement', count: 1 });
    };

    return <button onClick={handleClick}>-</button>;
};

export default DecrementBtn;

এবার ব্রাউজারে গেলে রিডাক্স ডেভ টুলে দেখলে দেখবো হিস্টোরি আপডেট হচ্ছে।

এবার পালা History কম্পোনেন্টের।

import { useStoreActions, useStoreState } from 'easy-peasy';

const History = () => {
    const { items } = useStoreState((state) => state.history);
    const { clearHistory } = useStoreActions((actions) => actions.history);

    return (
        <div>
            <p>
                Histories <button onClick={clearHistory}>Clear History</button>
            </p>
            <ul>
                {items &&
                    items.map((item) => (
                        <li key={item.id}>
                            {' '}
                            {item.action} - {item.count} - {item.time.toISOString()}{' '}
                        </li>
                    ))}
            </ul>
        </div>
    );
};

export default History;

আমাদের হিস্টোরি কম্পোনেন্টও ঠিকভাবে কাজ করছে।

মোটামুটি রিডাক্স এবং easy-peasy নিয়ে একটা ধারণা আপনারা পেলেন। আশা করি বুঝতে পেরেছেন। easy-peasy ছাড়াও আপনি redux-toolkit ব্যবহার করতে পারেন। কোনো সমস্যা নেই।

Source Code:

এই লেকচারে সমস্ত সোর্স কোড এই লিংকে পাবেন।