Container-Presentational Pattern
Container-Presentational Pattern হলো Separation of Concerns (SoC) প্রিন্সিপাল
মেনে কম্পোনেন্ট ডিজাইন করার একটি পদ্ধতি। এই ক্ষেত্রে বা এই প্যাটার্নের ক্ষেত্রে
আমাদের UI (Presentational) Component এবং Logic (Container) Component আলাদা আলাদা
করি।
যেমন ধরি Parent Component এর কাছে সকল প্রকার logic, Data fetching এসব করা হল এবং
Child Component সেগুলা শুধু রেন্ডার করা কাজ করবে । এক্ষেত্রে সুবিধা হবে Child
Component গুলো চাইলে আমরা অন্যান্য নানা জায়গায় ব্যবহার করতে পারব , অর্থাৎ কোডের
রি-ইউজিবিলিটি বাড়বে ও আমাদের কম কোড লেখা লাগবে। এক্ষেত্রে একটা জিনিস মাথায় রাখা
লাগবে যে আমাদের Child Component গুলো যত সম্ভব Dumb Component বানাতে হবে, এখানে
Dumb Component বলতে কি বুঝানো হয়েছে? Dumb Component বলতে এসব Component বিশেষ
লজিক ধারণ করবে না ফলে এটাকে প্রপস পাঠিয়ে আমরা নানা জায়গায় ব্যবহার করতে পারব
| বিষয় | Container Component | Presentational Component |
|---|---|---|
| কাজ | Data fetching, state management, business logic হ্যান্ডেল করা | শুধুমাত্র UI এবং props অনুযায়ী রেন্ডারিং করা |
| Dependency | External API, Redux, Context, Hooks ব্যবহার করতে পারে | শুধুমাত্র UI রেন্ডার করে, external dependency কম থাকে |
| State Management | Stateful হতে পারে (useState, useEffect ব্যবহার করতে পারে) | Stateless হয়, শুধুমাত্র props-এর ওপর নির্ভর করে |
| Responsibility | UI Component-এ props পাঠানো ও event handler সেট করা | UI প্রদর্শন করা ও props থেকে ডাটা দেখানো |
| Code Reusability | একটি container কম্পোনেন্ট বিভিন্ন UI কম্পোনেন্টের জন্য data handle করতে পারে | একই UI বিভিন্ন জায়গায় পুনরায় ব্যবহার করা যায় |
এখন আমরা সাধারণভাবে ডিজাইন করে দেখি ।
vue.js এর উদাহরণ নিচের দিকে থাকবেউদাহরণঃ
import { useEffect, useState } from "react";
interface User {
id: number;
name: string;
email: string;
}
const UserList = () => {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((data) => setUsers(data))
.catch((err) => console.error("Error fetching users:", err));
}, []);
return (
<div className="rounded-lg bg-gray-100 p-4">
<h2 className="mb-2 text-lg font-bold">User List</h2>
<ul>
{users.map((user) => (
<li key={user.id} className="border-b p-2">
<strong>{user.name}</strong> - {user.email}
</li>
))}
</ul>
</div>
);
};
export default UserList;
এখানে আমরা সহজ একটা উদাহরণ নিলাম যেখানে, একই Component এর মধ্যেই Data Fetcing ও পাশাপাশি আবার সেটা UI তে দেখানোও হচ্ছে । এখানে একটা সমস্যা আছে, সেটা হল যদি আমাদের এই API এর নিয়ে কোন কাজ করতে হয় কখনও তখন সেটা আমাদের এই UI এর দিকটাও দেখতে হছে , যেখানে একটা লম্বা কোড বেস হবে সেখানে । যা মেন্টেইন করতে কষ্ট হতে পারে। আবার এখানে যদি আরও এমন থাকত তখন সিনারিওটা কেমন হতে পারত চিন্তা করা যায় ?
এখন আমরা এটাকে সহজ ভাবে করব, Container-Presentational Pattern অনুসারে করলে এটা কেমন হত সেটা দেখি এখনঃ
১ . Container Component (UserListContainer.tsx)
import { useEffect, useState } from "react";
import UserList from "./UserList";
const UserListContainer = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((data) => setUsers(data))
.catch((err) => console.error("Error fetching users:", err));
}, []);
return <UserList users={users} />;
};
export default UserListContainer;
২ . Presentational Component (UserList.tsx)
interface User {
id: number;
name: string;
email: string;
}
const UserList = ({ users }: { users: User[] }) => {
return (
<div className="rounded-lg bg-gray-100 p-4">
<h2 className="mb-2 text-lg font-bold">User List</h2>
<ul>
{users.map((user) => (
<li key={user.id} className="border-b p-2">
<strong>{user.name}</strong> - {user.email}
</li>
))}
</ul>
</div>
);
};
export default UserList;
সুবিধা কি ?
এখানে আমরা কি এমন এক্সট্রা সুবিধা পাচ্ছি সেটা বিবেচনা করি ;
- Code Maintainability : Logic এবং UI আলাদা থাকায় কোড বুঝতে ও মেইনটেইন করতে সহজ হয়। যদি এখানে আরও অনেক কোড থাকত তখন সেটা মেইন্টেইন করা ও কোড রিড করা কষ্টদায়ক হত ।
- Reusability : UserList কম্পোনেন্টকে অন্য জায়গায়ও ব্যবহার করা যাবে, এটাকে চাইলেই আমরা নানা ভাবে নানা জায়গায় এই Data কে Props হিসেবে দিয়েই ব্যবহার করতে পারব ।
- Scalability : প্রজেক্ট বড় হলেও সমস্যা হবে না, কারণ কম্পোনেন্টগুলো স্পষ্টভাবে আলাদা থাকে।
- Testing সহজ হয় : Presentational Component গুলো সহজে টেস্ট করা যায় কারণ তারা
শুধুমাত্র UI রেন্ডার করে। যেমন আমরা চাইলে data fetching টাও অন্য
fileএ করে সেটা এখানে ব্যবহার করতে পারতাম, এক্ষেত্রে সেই ফাইলে সহজেই unit test করা যেত ।
সুতরাং, এসকল সুবিধার জন্য আমরা মূলত এই Pattern ব্যবহার করতে পারি । এক্ষেত্রে এই প্যাটার্ন Separation of concerns priciple ফলো করে ও সেটাকে মেনে চলে । অর্থাৎ Separation of concerns ও ঠিক এই বিষয়ে বলে আর আমরা এই ডিজাইন প্যাটার্ন মেনে চললে সেটাই তখন Separation of concerns এর নিয়ম সন্তুষ্ট করে ।
Single Responsibility Principle (SRP)
এখন আমরা এই ধরনের আরেকটা ইম্পোর্টেন্ট প্রিসিসিপাল বা কনভেনশন সম্পর্কে জানব আর সেটা হল Single Responsibility Principle । এর নাম দিয়েই এটা অনেকটা বুঝা যায় , Single Responsibility Principle এর মানে হল কোন নির্দিষ্ট একটা Component টি কোণ নির্দিষ্ট একটা কাজ করবে অর্থাৎ Single Responsibility ধারণ করবে তা নিয়েই কাজ করবে ।
Single Responsibility Principle ও Separation of concerns Principle অনেকটা সিমিলার যদিও বেশ পার্থক্য আছে ,
যেমন,
-
Separation of concerns Principle অন্যযায়ী UI and business logic, state আলাদা হবে এখানে parent Component চাইলে একাধিক data fetching বা business logic রাখতে পারব
-
Single Responsibility Principle অন্যযায়ী তা হবে না , কোন নির্দিষ্ট
Fileসে নির্দিষ্ট একটা কাজই করবে বা উদাহরণ হিসেবে বলতে পারি যেমন সে একটা বিষয় Data Fetch করে সেটা UI তে রেন্ডার করবে ।SRP এর উদাহরণ
আমরা আমাদের আগের উপরের আগের কোড এর উদাহরণ দেখি । সেখানে আমরা রি-ফ্যাক্টর করে আলাদা কম্পোনেন্ট করছিলাম কিন্তু আমরা fetch এর লগিক আলদা করিনি, যদি আমাদের সেম ডাটা কোথাও আবার fetch করতে হয় তখন সেটা আবার আমাদের লিখতে হবে । আবার এখানে, Container-Presentational Pattern এর প্রিসিপাল স্যাটিস্ফাই করলেও Single Responsibility Principle সেটিস্ফাই করতে পারেনি, এর কারণ ? মূলত এখানে আমাদের Container Component কি কাজ করছে ?
- Data Fetch করছে ও সেটার লজিক হ্যান্ডেল করছে ।
- সেটা child components এর কাছে পাঠিয়েছিয়ে ।
অর্থাৎ এখানে কিন্তু এর ২টা কাজ করতে হচ্ছে । এক্ষেত্রে যদি আমরা Single Responsibility Principle কেও সন্তুষ্ট করতে চাই তাহলে সে হিসেবে আমাদের প্রতি components, hook এর নির্দিষ্ট একটা কাজই করতে হবে । উদাহরণঃ
container / parent components
import useUsers from "../hooks/useUsers";
import UserList from "./UserList";
const UserListContainer = () => {
const { users, error, loading } = useUsers();
if (loading) return <p>Loading users...</p>;
if (error) return <p className="text-red-500">{error}</p>;
return <UserList users={users} />;
};
export default UserListContainer;
custom hook for data fetching about user
import { useEffect, useState } from "react";
interface User {
id: number;
name: string;
email: string;
}
const useUsers = () => {
const [users, setUsers] = useState<User[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!response.ok) throw new Error("Failed to fetch users");
const data = await response.json();
setUsers(data);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
return { users, error, loading };
};
export default useUsers;
বিশ্লেষণঃ
- data fetching logic আমরা useUsers নামের একটা কাস্টম Hooks এর মধ্যে নিয়ে গেলাম ।
- container থেকেই এর loading, error এবং data দেখানোর জন্য UserList component এর কাছে পাঠিয়েছিয়ে । (error,data, loading এগুলা একটা
group of workবা রিলেটেড single কাজই ) - UserList এর কার্ড এর কাজ হল যদি data পাঠানো হয় তাহলে সেটা UI তে দেখানো । এক্ষেত্রে এটাকে চাইলে অন্য সব জায়গায়ও ভিন্ন ভিন্ন ডাটা দিয়ে ব্যবহার করতে পারব ।
vue.js এর সাথে Container-Presentational Pattern এর উদাহরণ
উদাহরণ হিসেবে আমরা সাধারণ একটা কোম্পানেন্ট বানিয়ে নেই ।
<script setup>
import { ref, onMounted } from "vue";
const users = ref([]);
onMounted(async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
users.value = await response.json();
} catch (error) {
console.error("Error fetching users:", error);
}
});
</script>
<template>
<div class="rounded-lg bg-gray-100 p-4">
<h2 class="mb-2 text-lg font-bold">User List</h2>
<ul>
<li v-for="user in users" :key="user.id" class="border-b p-2">
<strong>{{ user.name }}</strong> - {{ user.email }}
</li>
</ul>
</div>
</template>
Container-Presentational Pattern ফলো করে এইবার আমরা করি ।
1: Container Component (UserListContainer.vue)
<!-- src/components/UserListContainer.vue -->
<script setup>
import { ref, onMounted } from "vue";
import UserList from "./UserList.vue";
const users = ref([]);
onMounted(async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
users.value = await response.json();
} catch (error) {
console.error("Error fetching users:", error);
}
});
</script>
<template>
<UserList :users="users" />
</template>
2: Presentational Component (UserList.vue)
<!-- src/components/UserList.vue -->
<script setup>
defineProps({
users: Array,
});
</script>
<template>
<div class="rounded-lg bg-gray-100 p-4">
<h2 class="mb-2 text-lg font-bold">User List</h2>
<ul>
<li v-for="user in users" :key="user.id" class="border-b p-2">
<strong>{{ user.name }}</strong> - {{ user.email }}
</li>
</ul>
</div>
</template>