রিডাক্স (redux) নিয়ে কাজ করার সময় বিশেষ করে রিয়েক্ট (react) -এ রিডাক্সের ভূমিকা কি এটা নিয়ে প্রথম দিকে একটু বিভ্রান্তি হয়, অন্তত আমার হয়েছে। এটা হতে পারে রিয়েক্ট-এর ডকুমেন্টেশনে উদাহরণগুলো এতই স্বয়ং সম্পূর্ণ যে এখানে রিডাক্স কিভাবে ব্যবহার করবো তা নিয়ে ভেবাচেকা খেতে হয়। যাই হোক, আমি এই লেখায় রিডাক্স কি, রিডাক্স কেন এবং কোথায় আমরা রিডাক্স ব্যবহার করতে পারবো সেটা নিয়ে আলোচনা করবো।
সহজ কথায় রিডাক্স হচ্ছে অ্যাপ্লিকেশনের স্টেট (state) ম্যানেজ করার জন্য একটি লাইব্রেরি। আমরা যারা জাভাস্ক্রিপ্ট-এ কাজ করি তারা জানি যে, স্টেট হলো অবজেক্টের প্রোপার্টিজ অথবা প্রোপার্টিজের সমষ্টি যাদেরকে আমরা আরেক কথায় মডেল (model) বলে থাকি। যেহতু রিডাক্স একটি লাইব্রেরি, আমরা একে যেকোনো সিঙ্গেল পেইজ অ্যাপ্লিকেশনে ব্যবহার করতে পারি। আমি এখানে সিঙ্গেল পেইজ অ্যাপ্লিকেশন বললাম কারণ সাধারণত ক্লায়েন্ট এন্ড-এ সিঙ্গেল পেইজ অ্যাপ্লিকেশনেই স্টেট ম্যানেজমেন্ট করার প্রয়োজন হয়।
প্রথমে আমাদের যা জানতে হবে, সেটা হচ্ছে রিডাক্স মূলত তিনটি মূল নীতি অনুসরন করে,
১। সম্পূর্ণ অ্যাপ্লিকেশনে একটি মাত্র স্টেট ট্রি বা অবজেক্ট ট্রি (single state tree or object tree) থাকবে। এর সুবিধা হলো খুব সহজেই অ্যাপ্লিকেশন ডিবাগ করা যায় এবং ডাটার সিঙ্গেল সোর্স অফ ট্রুথ এর নিশ্চিত করে। খুব সহজেই আমরা সার্ভার থেকে স্টেট ট্রি অ্যাপ্লিকেশনে ইনজেক্ট করতে পারি।
২। স্টেটটি হবে রিড অনলি (read only) যা শুধু মাত্র অ্যাকশন ডিসপ্যাচ (dispatch) করার মাধ্যমে পরিবর্তন করা যাবে। তার মানে কখনো কারো যদি কারো স্টেট পরিবর্তন করার দরকার হয় সে শুধু নির্দিষ্ট অ্যাকশন ডিসপ্যাচ করবে। এখানে অ্যাকশন হলো একটি সিম্পল জাভাস্ক্রিপ্ট অবজেক্ট যা ওই অ্যাকশনের সকল তথ্য রাখে যেমন, কি টাইপের অ্যাকশন হবে, কোন ডাটা পরিবর্তন করা লাগবে ইত্যাদি।
৩। কোনো নির্দিষ্ট অ্যাকশন ডিসপ্যাচ করার পরে, স্টেট পরিবর্তন করতে হবে প্রয়োজনীয় পিউর ফাংশনের (Pure function) মাধ্যমে যাদেরকে রিডাক্স-এ রিডিউসার বলে। পিউর ফাংশন হল সেই ফাংশন যার ভিতরে কোন প্যাচগোচ নাই অর্থাৎ এটি কোন অ্যাসিনক্রোনাস অপারেশন করবে না, কোন আই/ও অপারেশন করবে না, এমনকি স্টেট অবজেক্টকে মিউটেট করতে পারবে না। তবে স্টেট অবজেক্টকে মিউটেট করার পরিবর্তে নতুন অবজেক্ট তৈরি করতে পারবে। কোন একটি অ্যাকশন ডিসপ্যাচ করলে রিডাক্স অ্যাপ্লিকেশনের প্রতিটি রিডিউসার ফাংশনকে কল করে এবং ফাংশনের আর্গুমেন্ট হিসেবে পাঠিয়ে দেয় পূর্বের স্টেট এবং অ্যাকশন অবজেক্টটি। রিডিউসার ফাংশনের কাজ হচ্ছে অ্যাকশনের উপর ভিত্তি পরবর্তী স্টেট নির্ধারণ করা। যেহেতু এই ফাংশনগুলোর কাজ অনেকটা জাভাস্ক্রিপ্ট reducer ফাংশনের মতো অর্থাৎ আগের স্টেট থেকে পরবর্তী স্টেট নির্ধারণ করা তাই এদেরকে রিডিউসার বলা হয়।
আরেকটি বিষয়, রিডাক্স শুধু স্টেট ম্যানেজমেন্ট করে না, তার সাথে স্টেট-এ কোনো পরিবর্তন হলে সেটাও অবহিত (notify) করে। অ্যাকশন ফাংশনগুলো পিউর ফাংশন হওয়ার কারণে স্টেট-এ কোনো পরিবর্তন হয়েছে কিনা সেটা বের করা রিডাক্সের জন্য খুব সহজ কেননা শুধুমাত্র ইকুয়ালিটি চেক করেই সেটা বের করা যায়। নিচের উদাহরণটি দেখলেই ব্য্যপারটি বোঝা যাবে।
var bookList = [ {id: 1, title:'Foundation Series'}, {id: 2, title:'The Da Vinci Code'} ]; /* Now we want to add a new book. But it is not permitted to mutate bookList! No problem, we can use contact() function which return a new array instead of mutating existing one. */ var newBookList = bookList.concat({id:3, title:'New book'}); var isChanged = (bookList !==newBookList); /* return true */
এবার আসা যাক কিভাবে আমরা রিডাক্স ব্যবহার করবো। ধরে নেই, আমরা একটি ছোট বুকলিস্ট অ্যাপ্লিকেশন বানাতে চাই, যেখানে আমরা বইয়ের লিস্ট দেখাবো এবং নতুন বই সংযোগ করতে পারবো। নতুন বই সংযোগ করলে সাথে সাথে পরিবর্তিত লিস্ট দেখাবে। আমরা যদি এটাকে ভ্যানিলা জাভাস্ক্রিপ্ট মানে কোন প্রকার ফ্রেমওয়ার্ক ছাড়া করি তা অনেকটা নিচের মতো হবে।
/* For managing booklist state, lets declare an array*/ var bookID=1; var myBooks = [ {id: 1, title:'Foundation Series'}, {id: 2, title:'The Da Vinci Code'} ]; /* Add book function */ function addBook(book) { myBooks.push(book); } /* For simplicity, lets use tabular list and update table inner html */ function renderToDOM() { var booklist = getFilterdBooks(), html; html = booklist.reduce(function(html,singleBook) { return html + `<tr> <td>${singleBook.id}</td> <td>${singleBook.title}</td> </tr>`; },''); document.getElementById('container').innerHTML = html; } /* For rendering initial list, invoke renderToDOM() first time */ renderToDOM(); /* whenever user click on add-btn, it will insert a new book and re-render to reflect new list. */ document.getElementById('add-btn').addEventListener('click', function() { addBook({id: bookID++, title: 'A new book' }); renderToDOM(); });
এখন যদি অ্যাপ্লিকেশনটি রিডাক্স-এ ইমপ্লিমেন্ট করতে চাই, তাহলে তার মূল নীতি অনুসারে কিছু নিয়ম অনুসরন করতে হবে। যেহেতু, রিডাক্স-এ স্টেট শুধুমাত্র পরিবর্তন করা যাবে কেবল মাত্র অ্যাকশন ডিসপ্যাচ করার মাধ্যমে, তাই অ্যাকশন অবজেক্টগুলো আগে তৈরি করা যাক। সাধারণত অ্যাকশন অবজেক্টগুলো তৈরি করা হয় অ্যাকশন ক্রিয়েটর ফাংশনের মাধ্যমে যেটি আমাদের অ্যাকশন অবজেক্টটি রিটার্ন করবে। উলেখ্য যে, বাঁকি উদাহরণগুলোতে আমি ES6 সিনট্যাক্স ব্যবহার করবো।
তো অ্যাকশন ক্রিয়েটর অনেকটা নিচের মতো হবেঃ
let bookID = 1; /* For making unique bookID */ export const addBook = (title) => ({ type: 'ADD_BOOK', id: bookID++, title })
আর রিডিউসার ফাংশনটি অনেকটা নিচের মতো হবেঃ
const books = (state = [], action) => { switch (action.type) { case 'ADD_BOOK': return state.concat( { id: action.id, title: action.title } ); default: return state; } } export default books;
মূলত প্রত্যেকটি রিডিউসার ফাংশন একটি সুইচ কেইস ব্লক যেখানে অ্যাকশনের টাইপ অনুসারে বিভিন্ন স্টেট রিটার্ন করে। যদি কোন অ্যাকশনের টাইপ না মিলে অর্থাৎ সুইচ কেইস ব্লকে না পরে, তাহলে ডিফল্ট হিসেবে পূর্ববর্তী স্টেট রিটার্ন করতে হবে। মনে রাখতে হবে যে, ডিফল্ট হিসেবে পূর্ববর্তী স্টেট রিটার্ন করা বাধ্যতামূলক কেননা রিডাক্স যেকোনো অ্যাকশনের জন্যে অ্যাপ্লিকেশনের সকল রিডিউসারকে কল করে এবং সেজন্য অ্যাকশন অবজেক্টটিতে টাইপ (type) প্রপার্টিজটি থাকতেই হবে, বাকি সব প্রপার্টিজ অ্যাপ্লিকেশানের প্রয়োজন মতো, কোন বাধ্যবাধকতা নাই।
এখন আসা যাক রিডাক্সের আসল ভূমিকায়। অ্যাকশন এবং রিডিউসারকে একীভূত করে রিডাক্সের যেই অবজেক্টেটি তার নাম স্টোর (store)। স্টোরের মূলত তিনটি প্রধান মেথড আছেঃ
১। স্টেটের বর্তমান অবস্থা পেতে getState()
২। অ্যাকশন ফায়ার অর্থাৎ ডিসপ্যাচ করতে dispatch(actionObject)
৩। স্টেটের কোনো পরিবর্তনে নটিফাই পেতে subscribe(function)
স্টোর তৈরি করতে রিডাক্সের createStore() মেথড ব্যবহার করতে হয় যা মূলত তিনটি আর্গুমেন্ট সাপোর্ট করে।
১। রিডিউসার
২। ইনিশিয়াল স্টেট
৩। অ্যাপ্লাই মিডিলওয়্যার
শুধুমাত্র প্রথম প্যারামিটার অর্থাৎ রিডিউসারটি বাধ্যতামূলক, বাঁকিগুলো অপশনাল। রিডিউসার ও ইনিশিয়াল স্টেট প্যারামিটারের নাম থেকেই বোঝা যাচ্ছে তাদের উদ্দেশ্য। অ্যাপ্লাই মিডিলওয়্যার ব্যবহার করা হয় স্টোরের ফাংশনালিটি আরো এক্সটেন্ড বা বর্ধিত করার জন্য। একটি উদাহরণ দেয়া যাক, যেহেতু অ্যাকশন অবজেক্টটি মূলত প্লেইন জাভাস্কিপ্ট অবজেক্ট, আমরা চাইলে কোনো অ্যাসিনক্রোনাস অপারেশন যেমন, সার্ভার থেকে ডাটা আনা, প্রমিজ নিয়ে কাজ করা ইত্যাদি করতে পারবো না। এই সমস্যা সমাধান করা যায় মিডিলওয়্যার দিয়ে, যেখানে মিডিলওয়্যারটি অ্যাসিনক্রোনাস অপারেশনটি করে প্রয়োজনীয় অ্যাকশনটি ডিসপ্যাচ করে দিতে পারে। অনেক মিডিলওয়্যার রেডিমেইড পাওয়া যায়, প্রয়োজন হলে নিজেরা তৈরি করে নেয়া যায়। খুব পরিচিত একটি মিডিলওয়্যার হচ্ছে redux-thunk যার মাধ্যমে অ্যাকশন অবজেক্টই স্থলে ফাংশন ব্যবহার করা যায়। খুব সংক্ষেপে মিডিলওয়্যার কিভাবে কাজ করে বলে যাই, মূলত স্টোর তৈরি করার সময় যদি কোন মিডিলওয়্যার প্রোভাইড করা হয়, রিডাক্স প্রতিটি অ্যাকশন ডিসপ্যাচ করার পূর্বে প্রথমে মিডিলওয়্যারটি কল করে এবং মিডিলওয়্যারটি যদি তার প্রত্যাশিত প্যাটার্ন অনুসারে মিল পায়,তাহলে সেটি তা প্রসেস করে এবং সেই অ্যাকশনটি আর রিডাক্সের কাছে না দিয়ে প্রয়োজনীয় কাজ শেষে নিজেই অ্যাকশনটি ডিসপ্যাচ করে দেয় আর যদি মিল না পায়, তাহলে অ্যাকশনটি পরবর্তী মিডিলওয়্যারের কাছে ফরওয়ার্ড করে, এভাবে সর্বশেষে রিডাক্সের কাছে পৌছায়।
যাই হোক, আমাদের ভ্যানিলা জাভাস্কিপ্ট অ্যাপ্লিকেশনটি রিডাক্সে-এ পরিবর্তন করতে আমরা renderToDOM() মেথডকে ষ্টোরের মাধ্যমে সাবস্ক্রাইব করবো যাতে ষ্টেট-ট্রিতে কোন পরিবর্তন হলে বুকলিস্ট নিজে নিজে আপডেট হয় আর নতুন বই সংযোগ করতে dispatch() -এর মাধ্যমে addBook() অ্যাকশন পাঠাবো এবং ইনিশিয়াল ষ্টেট হিসেবে আমাদের বইয়ের লিস্টটি দিয়ে দিবো। সবশেষে renderToDOM() -এ একটু পরিবর্তন করবো যাতে ষ্টেটটি এখনথেকে রিডাক্স স্টোর থেকে নেয়। অ্যাপ্লিকেশনের রিডাক্স ভার্সনটি নিচের মতো হবেঃ
{createStore} from 'redux'; const renderToDOM = () => { let state = store.getState(), booklist = state.books, html; html = booklist.reduce(function(html,singleBook) { return html + `<tr> <td>${singleBook.id}</td> <td>${singleBook.title}</td> </tr>`; },''); document.getElementById('container').innerHTML = html; }; let initialState = [ {id: 1, title:'Foundation Series'}, {id: 2, title:'The Da Vinci Code'} ]; const store = createStore( books, initialState ); store.subscribe(()=>{ renderToDOM(); }); /* whenever user click on add-btn, it will insert a new book and re-render to reflect new list. */ document.getElementById('add-btn').addEventListener('click', function() { store.dispatch(addBook({title: 'A new book' })); });
লক্ষণীয় যে, createStore() মেথড শুধুমাত্র একটি রিডিউসার সাপোর্ট করে। যেহেতু রিডাক্স, রিডিউসারগুলো থেকে ষ্টেট-ট্রি তৈরি করে এবং একটি অ্যাপ্লিকেশনে একটিই স্টেট-ট্রি থাকে তাই রিডিউসারও হয় একটি। কিন্তু একটি অ্যাপ্লিকেশনে একাধিক রিডিউসারের প্রয়োজনীয়তা খুবই স্বাভাবিক। একাধিক রিডিউসার থাকলে তাদেরকে একীভূত করার জন্যে রিডাক্সের combineReducers() নামে একটি মেথড আছে যেখানে একাধিক রিডিউসার ইনপুট দিলে একটি কম্বাইন্ড রিডিউসার রিটার্ন করে এবং সেই কম্বাইন্ড রিডিউসার ব্যবহার করে createStore() -এর মাধ্যমে স্টোর তৈরি করতে হয়।
এখন আমরা যদি আমাদের অ্যাপ্লিকেশনটি শুধু রিয়েক্ট (react)-এ কনভার্ট করি তা অনেকটা নিচের মতো হবে। আমি এখানে দুটি কম্পোনেন্ট ব্যবহার করেছি। একটি প্রেসেন্টেশনাল কম্পোনেন্ট, বইয়ের লিস্ট দেখানোর জন্যে এবং আরেকটি কন্টেইনার কম্পোনেন্ট, স্টেট ম্যানেজমেন্ট এবং অন্যান্য DOM ইভেন্ট হ্যান্ডলিংয়ের জন্যে।
const BookList = (props) => { return ( <tbody id="container"> { props.books.map((book)=> { return ( <tr> <td>{book.id}</td> <td>{book.title}</td> </tr> ); }) } </tbody> ); } class App extends Component { constructor(props) { super(props); this.state = { books: [] }; } componentDidMount() { let books = [ { id: 1, title: 'Around the World in Eighty Days' }, { id: 2, title: 'Foundation Series' } ]; this.setState({ books: books }); } render() { return ( <table> <Booklist books={this.state.books} /> </table> ); } } /* Attached App to a DOM node */ ReactDOM.render( <App />, document.getElementById('root') );
এখন যদি রিয়েক্ট অ্যাপ্লিকেশনটির ষ্টেট ম্যানেজমেন্টের দায়িত্ব রিডাক্সকে দিতে চাই, তাহলে আমাদের যা করতে হবেঃ
১। রিয়েক্ট-এ রিডাক্স ব্যবহার করার জন্যে, রিডাক্সের একটি লাইব্রেরি আছে যার নাম react-redux।
২। react-redux লাইব্রেরির মূল ভূমিকা হচ্ছে রিডাক্স ষ্টেট থেকে প্রয়োজনীয় প্রপার্টিজগুলোকে রিয়েক্ট-এর কম্পোনেন্ট-এ props হিসেবে দিয়ে দেয়া এবং ষ্টেট-এ কোনো পরিবর্তন হলে রিয়েক্ট-এর render() মেথড কল করে দেয়া যাতে রিয়েক্ট-এর ভিউ সবসময় আপডেটেড থাকে।
৩। রিডাক্সের স্টেট এবং রিয়েক্ট কম্পোনেটের মধ্যে সেতু বন্ধন করার জন্যে react-redux লাইব্রেরি connect() মেথড ব্যবহার করতে হয়।
৪। connect() মেথড এ দুটি আর্গুমেন্ট পাঠানো যায়। একটি হলো, স্টেট-ট্রির কোন প্রোপার্টিজ রিয়েক্ট props-এ কি নামে পাওয়া যাবে তার একটি ম্যাপ এবং দ্বিতীয়টি হলো, রিয়েক্ট থেকে রিডাক্সের অ্যাকশন ডিসপ্যাচ করার জন্যে, যে সব অ্যাকশন রিয়েক্ট-এ প্রয়োজন তার আরেকটি ম্যাপ।
৫। এই connect() মেথড যেকোনো রিয়েক্ট কম্পোনেন্টে ব্যবহার করা যেতে পারে। react-redux লাইব্রেরির পবিত্র দায়িত্ব হচ্ছে স্টেট ম্যাপটি props মাধ্যমে সেখানে হাজির করা। তবে এই জন্য আরেকটি শর্ত মানতে হয়, তা হলো react-redux লাইব্রেরি বিনা পয়সায় Provider নামে একটি রিয়েক্ট কম্পোনেন্ট দেয়, সেটাকে অ্যাপ্লিকেশনের রুট বা টপ কম্পোনেট হিসেবে ব্যবহার করতে হবে এবং রিডাক্স স্টোরটি props হিসেবে দিয়ে দিতে হবে। খুবই ন্যায্য দাবী না হলে রিডাক্স বেচারা স্টোরটি পাবে কোথায়!
প্রেসেন্টেশনাল কম্পোনেন্টে কোনো পরিবর্তন লাগবে না, শুধু কন্টেইনার কম্পোনেন্ট App -এ সব কিছু দিয়ে সাজালে গুছালে নিচের মতো হবে।
class App extends Component { constructor(props) { super(props); } /* We should be use books from props instead of state now*/ render() { return ( <table> <Booklist books={this.props.books} /> </table> ); } } /* state mapping that we want to attach into react props. We need to define map inside a function and state will be available through parameter */ const mapStateToProps = (state) => { return { books: state.books, }; }; /* dispatch mapping that we want to attach into react props. We need to define map inside a function and dispatch will be available through parameter */ const mapDispatchToProps = (dispatch) => { return { addBook: (title, author, status) => dispatch(addBook(title)) }; }; /* connect is a curried function, first it need to provide props map and then dispatch maps. It will return another method where the component will need to be provided */ export default connect(mapStateToProps, mapDispatchToProps)(App); /* We must to use Provider component gifted by react-redux and store should be set as pros */ ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
সবশেষে আমি একটি ছোট প্রোজেক্ট মোট চার ভাবে মানে ভ্যানিলা থেকে রিয়েক্ট-রিডাক্স -এ করে দেখিয়েছি যাতে পাথক্যটা ভালোভাবে বোঝা যায়। তাছাড়া এখানে একাধিক রিডিউসারের ব্যবহার এবং নতুন বই অ্যাড, বই এডিট, বই ডিলিট করলে পিউর ফাংশনগুলো অবজেক্ট মিউটেট না করে কিভাবে করা যায় তার উদাহরণ দিয়েছি। প্রোজেক্টির গিটহাব লিঙ্ক হচ্ছে VanillaJS-to-react-redux।