React-patterns / Container-Presentational Pattern

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 ComponentPresentational Component
কাজData fetching, state management, business logic হ্যান্ডেল করাশুধুমাত্র UI এবং props অনুযায়ী রেন্ডারিং করা
DependencyExternal API, Redux, Context, Hooks ব্যবহার করতে পারেশুধুমাত্র UI রেন্ডার করে, external dependency কম থাকে
State ManagementStateful হতে পারে (useState, useEffect ব্যবহার করতে পারে)Stateless হয়, শুধুমাত্র props-এর ওপর নির্ভর করে
ResponsibilityUI 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;

সুবিধা কি ?

এখানে আমরা কি এমন এক্সট্রা সুবিধা পাচ্ছি সেটা বিবেচনা করি ;

সুতরাং, এসকল সুবিধার জন্য আমরা মূলত এই 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 অনেকটা সিমিলার যদিও বেশ পার্থক্য আছে ,

যেমন,

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;

বিশ্লেষণঃ

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>