বাংলা

কোড স্প্লিটিং-এর বাইরে ডেটা ফেচিং-এর জন্য React Suspense অন্বেষণ করুন। Fetch-As-You-Render, এরর হ্যান্ডলিং, এবং গ্লোবাল অ্যাপ্লিকেশনের জন্য ভবিষ্যৎ-প্রমাণ প্যাটার্নগুলি বুঝুন।

React Suspense রিসোর্স লোডিং: আধুনিক ডেটা ফেচিং প্যাটার্নে দক্ষতা অর্জন

ওয়েব ডেভেলপমেন্টের গতিশীল জগতে, ইউজার এক্সপেরিয়েন্স (UX) সর্বোচ্চ গুরুত্ব রাখে। নেটওয়ার্কের অবস্থা বা ডিভাইসের ক্ষমতা নির্বিশেষে অ্যাপ্লিকেশনগুলোকে দ্রুত, প্রতিক্রিয়াশীল এবং আনন্দদায়ক হতে হয়। React ডেভেলপারদের জন্য, এর অর্থ প্রায়ই জটিল স্টেট ম্যানেজমেন্ট, লোডিং ইন্ডিকেটর এবং ডেটা ফেচিং ওয়াটারফলের বিরুদ্ধে একটি অবিরাম সংগ্রাম। এই সমস্যার সমাধান নিয়ে এসেছে React Suspense, একটি শক্তিশালী, যদিও প্রায়শই ভুল বোঝা হয় এমন ফিচার, যা অ্যাসিঙ্ক্রোনাস অপারেশন, বিশেষ করে ডেটা ফেচিং পরিচালনা করার পদ্ধতিকে মৌলিকভাবে পরিবর্তন করার জন্য ডিজাইন করা হয়েছে।

প্রাথমিকভাবে React.lazy() এর সাথে কোড স্প্লিটিং-এর জন্য এটি পরিচিত হলেও, Suspense-এর আসল শক্তি হলো যেকোনো অ্যাসিঙ্ক্রোনাস রিসোর্স, যেমন API থেকে ডেটা লোড করার ক্ষমতাকে সমন্বয় করা। এই বিশদ নির্দেশিকাটি রিসোর্স লোডিং-এর জন্য React Suspense-এর গভীরে প্রবেশ করবে, এর মূল ধারণা, মৌলিক ডেটা ফেচিং প্যাটার্ন এবং পারফরম্যান্ট ও স্থিতিস্থাপক গ্লোবাল অ্যাপ্লিকেশন তৈরির জন্য ব্যবহারিক বিবেচনার বিষয়গুলো অন্বেষণ করবে।

React-এ ডেটা ফেচিং-এর বিবর্তন: ইম্পারেটিভ থেকে ডিক্লেয়ারেটিভ

বহু বছর ধরে, React কম্পোনেন্টে ডেটা ফেচিং মূলত একটি সাধারণ প্যাটার্নের উপর নির্ভর করত: একটি API কল শুরু করার জন্য useEffect হুক ব্যবহার করা, useState দিয়ে লোডিং এবং এরর স্টেট পরিচালনা করা এবং এই স্টেটগুলোর উপর ভিত্তি করে শর্তসাপেক্ষে রেন্ডার করা। যদিও এটি কার্যকরী ছিল, এই পদ্ধতিতে প্রায়শই বেশ কয়েকটি চ্যালেঞ্জ দেখা দিত:

Suspense ছাড়া একটি সাধারণ ডেটা ফেচিং পরিস্থিতি বিবেচনা করুন:

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setIsLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        setUser(data);
      } catch (e) {
        setError(e);
      } finally {
        setIsLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (isLoading) {
    return <p>Loading user profile...</p>;
  }

  if (error) {
    return <p style={"color: red;"}>Error: {error.message}</p>;
  }

  if (!user) {
    return <p>No user data available.</p>;
  }

  return (
    <div>
      <h2>User: {user.name}</h2>
      <p>Email: {user.email}</p>
      <!-- More user details -->
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Welcome to the Application</h1>
      <UserProfile userId={"123"} />
    </div>
  );
}

এই প্যাটার্নটি সর্বত্র প্রচলিত, কিন্তু এটি কম্পোনেন্টকে তার নিজস্ব অ্যাসিঙ্ক্রোনাস স্টেট পরিচালনা করতে বাধ্য করে, যা প্রায়শই UI এবং ডেটা ফেচিং লজিকের মধ্যে একটি দৃঢ়ভাবে সংযুক্ত সম্পর্ক তৈরি করে। Suspense একটি আরও ডিক্লেয়ারেটিভ এবং সুবিন্যস্ত বিকল্প প্রদান করে।

কোড স্প্লিটিং-এর বাইরে React Suspense বোঝা

বেশিরভাগ ডেভেলপাররা প্রথমবার Suspense-এর সাথে পরিচিত হন React.lazy() এর মাধ্যমে কোড স্প্লিটিং-এর জন্য, যেখানে এটি একটি কম্পোনেন্টের কোড প্রয়োজন না হওয়া পর্যন্ত লোড করা স্থগিত করতে দেয়। উদাহরণস্বরূপ:

import React, { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import('./MyHeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading component...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

এই পরিস্থিতিতে, যদি MyHeavyComponent এখনও লোড না হয়ে থাকে, তাহলে <Suspense> বাউন্ডারি lazy() দ্বারা থ্রো করা প্রমিসটি ধরবে এবং কম্পোনেন্টের কোড প্রস্তুত না হওয়া পর্যন্ত fallback প্রদর্শন করবে। এখানে মূল বিষয়টি হলো, Suspense রেন্ডারিংয়ের সময় থ্রো করা প্রমিসগুলো ধরে কাজ করে

এই পদ্ধতিটি শুধুমাত্র কোড লোড করার জন্য সীমাবদ্ধ নয়। রেন্ডারিংয়ের সময় কল করা যেকোনো ফাংশন যা একটি প্রমিস থ্রো করে (যেমন, একটি রিসোর্স এখনও উপলব্ধ না হওয়ায়) তা কম্পোনেন্ট ট্রি-এর উপরের একটি Suspense বাউন্ডারি দ্বারা ধরা যেতে পারে। যখন প্রমিসটি রিজলভ হয়, React কম্পোনেন্টটি পুনরায় রেন্ডার করার চেষ্টা করে, এবং যদি রিসোর্সটি এখন উপলব্ধ থাকে, তবে ফলব্যাকটি লুকিয়ে রাখা হয় এবং আসল কন্টেন্টটি প্রদর্শিত হয়।

ডেটা ফেচিং-এর জন্য Suspense-এর মূল ধারণা

ডেটা ফেচিং-এর জন্য Suspense ব্যবহার করতে, আমাদের কয়েকটি মূল নীতি বুঝতে হবে:

১. একটি Promise থ্রো করা

প্রথাগত অ্যাসিঙ্ক্রোনাস কোড যা প্রমিস রিজলভ করতে async/await ব্যবহার করে, তার বিপরীতে Suspense এমন একটি ফাংশনের উপর নির্ভর করে যা ডেটা প্রস্তুত না থাকলে একটি প্রমিস *থ্রো* করে। যখন React এমন একটি কম্পোনেন্ট রেন্ডার করার চেষ্টা করে যা এই ধরনের ফাংশন কল করে, এবং ডেটা এখনও পেন্ডিং থাকে, তখন প্রমিসটি থ্রো হয়। React তখন সেই কম্পোনেন্ট এবং তার চাইল্ড কম্পোনেন্টগুলোর রেন্ডারিং 'বিরত' রাখে এবং নিকটতম <Suspense> বাউন্ডারি খোঁজে।

২. Suspense বাউন্ডারি

<Suspense> কম্পোনেন্টটি প্রমিসের জন্য একটি এরর বাউন্ডারি হিসাবে কাজ করে। এটি একটি fallback প্রপ নেয়, যা হল সেই UI যা রেন্ডার করা হবে যখন এর কোনো চাইল্ড (বা তাদের ডিসেন্ড্যান্ট) সাসপেন্ড করছে (অর্থাৎ, একটি প্রমিস থ্রো করছে)। যখন এর সাবট্রি-এর মধ্যে থ্রো করা সমস্ত প্রমিস রিজলভ হয়, তখন ফলব্যাকটি আসল কন্টেন্ট দ্বারা প্রতিস্থাপিত হয়।

একটি একক Suspense বাউন্ডারি একাধিক অ্যাসিঙ্ক্রোনাস অপারেশন পরিচালনা করতে পারে। উদাহরণস্বরূপ, যদি আপনার একই <Suspense> বাউন্ডারির মধ্যে দুটি কম্পোনেন্ট থাকে, এবং প্রতিটির ডেটা ফেচ করার প্রয়োজন হয়, তবে ফলব্যাকটি *উভয়* ডেটা ফেচ সম্পূর্ণ না হওয়া পর্যন্ত প্রদর্শিত হবে। এটি আংশিক UI দেখানো এড়ায় এবং একটি আরও সমন্বিত লোডিং অভিজ্ঞতা প্রদান করে।

৩. ক্যাশে/রিসোর্স ম্যানেজার (ইউজারল্যান্ডের দায়িত্ব)

গুরুত্বপূর্ণভাবে, Suspense নিজে ডেটা ফেচিং বা ক্যাশিং পরিচালনা করে না। এটি শুধুমাত্র একটি সমন্বয় ব্যবস্থা। ডেটা ফেচিং-এর জন্য Suspense-কে কাজ করাতে, আপনার একটি লেয়ার প্রয়োজন যা:

এই 'রিসোর্স ম্যানেজার' সাধারণত একটি সাধারণ ক্যাশে (যেমন, একটি Map বা একটি অবজেক্ট) ব্যবহার করে প্রতিটি রিসোর্সের অবস্থা (পেন্ডিং, রিজলভড, বা এররড) সংরক্ষণ করার জন্য প্রয়োগ করা হয়। যদিও আপনি প্রদর্শনের উদ্দেশ্যে এটি ম্যানুয়ালি তৈরি করতে পারেন, একটি বাস্তব-বিশ্বের অ্যাপ্লিকেশনে, আপনি একটি শক্তিশালী ডেটা ফেচিং লাইব্রেরি ব্যবহার করবেন যা Suspense-এর সাথে ইন্টিগ্রেট করে।

৪. কনকারেন্ট মোড (React 18-এর উন্নতি)

যদিও Suspense React-এর পুরোনো সংস্করণগুলিতে ব্যবহার করা যেতে পারে, এর সম্পূর্ণ শক্তি কনকারেন্ট React-এর সাথে উন্মোচিত হয় (React 18-এ createRoot এর সাথে ডিফল্টভাবে সক্রিয়)। কনকারেন্ট মোড React-কে রেন্ডারিং কাজ বাধাগ্রস্ত, বিরতি এবং পুনরায় শুরু করতে দেয়। এর মানে হল:

Suspense সহ ডেটা ফেচিং প্যাটার্ন

আসুন Suspense-এর আগমনের সাথে ডেটা ফেচিং প্যাটার্নের বিবর্তন অন্বেষণ করি।

প্যাটার্ন ১: Fetch-Then-Render (Suspense র‍্যাপিং সহ প্রচলিত পদ্ধতি)

এটি হল ক্লাসিক পদ্ধতি যেখানে ডেটা ফেচ করা হয়, এবং শুধুমাত্র তারপরেই কম্পোনেন্টটি রেন্ডার করা হয়। যদিও এটি ডেটার জন্য সরাসরি 'থ্রো প্রমিস' পদ্ধতি ব্যবহার করে না, আপনি একটি কম্পোনেন্ট যা *অবশেষে* ডেটা রেন্ডার করে, তাকে একটি Suspense বাউন্ডারিতে মুড়িয়ে একটি ফলব্যাক প্রদান করতে পারেন। এটি মূলত Suspense-কে একটি জেনেরিক লোডিং UI অর্কেস্ট্রেটর হিসাবে ব্যবহার করার বিষয়ে, সেইসব কম্পোনেন্টের জন্য যা অবশেষে প্রস্তুত হয়, এমনকি যদি তাদের অভ্যন্তরীণ ডেটা ফেচিং এখনও প্রচলিত useEffect ভিত্তিক হয়।

import React, { Suspense, useState, useEffect } from 'react';

function UserDetails({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchUserData = async () => {
      setIsLoading(true);
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      setUser(data);
      setIsLoading(false);
    };
    fetchUserData();
  }, [userId]);

  if (isLoading) {
    return <p>Loading user details...</p>;
  }

  return (
    <div>
      <h3>User: {user.name}</h3>
      <p>Email: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Fetch-Then-Render Example</h1>
      <Suspense fallback={<div>Overall page loading...</div>}>
        <UserDetails userId={"1"} />
      </Suspense>
    </div>
  );
}

সুবিধা: বোঝা সহজ, পশ্চাৎ-সামঞ্জস্যপূর্ণ। একটি গ্লোবাল লোডিং স্টেট যোগ করার একটি দ্রুত উপায় হিসাবে ব্যবহার করা যেতে পারে।

অসুবিধা: UserDetails-এর ভিতরে বয়লারপ্লেট দূর করে না। কম্পোনেন্টগুলো ক্রমানুসারে ডেটা ফেচ করলে এখনও ওয়াটারফলের প্রবণতা থাকে। ডেটার জন্য Suspense-এর 'থ্রো-এবং-ক্যাচ' পদ্ধতিকে সত্যিকার অর্থে ব্যবহার করে না।

প্যাটার্ন ২: Render-Then-Fetch (রেন্ডারের ভিতরে ফেচিং, প্রোডাকশনের জন্য নয়)

এই প্যাটার্নটি মূলত এটি দেখানোর জন্য যে Suspense-এর সাথে সরাসরি কী না করা উচিত, কারণ এটি সাবধানে পরিচালনা না করলে অসীম লুপ বা পারফরম্যান্স সমস্যার কারণ হতে পারে। এটিতে একটি কম্পোনেন্টের রেন্ডার পর্যায়ে সরাসরি ডেটা ফেচ করার বা একটি সাসপেন্ডিং ফাংশন কল করার চেষ্টা করা হয়, *একটি সঠিক ক্যাশিং ব্যবস্থা ছাড়াই*।

// একটি সঠিক ক্যাশিং লেয়ার ছাড়া এটি প্রোডাকশনে ব্যবহার করবেন না
// এটি শুধুমাত্র একটি সরাসরি 'থ্রো' কিভাবে ধারণাগতভাবে কাজ করতে পারে তার একটি উদাহরণ।

let fetchedData = null;
let dataPromise = null;

function fetchDataSynchronously(url) {
  if (fetchedData) {
    return fetchedData;
  }

  if (!dataPromise) {
    dataPromise = fetch(url)
      .then(res => res.json())
      .then(data => { fetchedData = data; dataPromise = null; return data; })
      .catch(err => { dataPromise = null; throw err; });
  }
  throw dataPromise; // এখানেই Suspense কাজ করে
}

function UserDetailsBadExample({ userId }) {
  const user = fetchDataSynchronously(`/api/users/${userId}`);
  return (
    <div>
      <h3>User: {user.name}</h3>
      <p>Email: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Render-Then-Fetch (Illustrative, NOT Recommended Directly)</h1>
      <Suspense fallback={<div>Loading user...</div>}>
        <UserDetailsBadExample userId={"2"} />
      </Suspense>
    </div>
  );
}

সুবিধা: দেখায় কিভাবে একটি কম্পোনেন্ট সরাসরি ডেটার জন্য 'জিজ্ঞাসা' করতে পারে এবং প্রস্তুত না থাকলে সাসপেন্ড করতে পারে।

অসুবিধা: প্রোডাকশনের জন্য অত্যন্ত সমস্যাযুক্ত। এই ম্যানুয়াল, গ্লোবাল fetchedData এবং dataPromise সিস্টেমটি সরল, একাধিক অনুরোধ, ইনভ্যালিডেশন বা এরর স্টেট সঠিকভাবে পরিচালনা করে না। এটি 'থ্রো-এ-প্রমিস' ধারণার একটি প্রাথমিক উদাহরণ, গ্রহণ করার মতো কোনো প্যাটার্ন নয়।

প্যাটার্ন ৩: Fetch-As-You-Render (আদর্শ Suspense প্যাটার্ন)

এটি হল সেই প্যারাডাইম শিফট যা Suspense সত্যিকার অর্থে ডেটা ফেচিংয়ের জন্য সক্ষম করে। একটি কম্পোনেন্টের ডেটা ফেচ করার জন্য রেন্ডার হওয়ার অপেক্ষা করার পরিবর্তে, বা সমস্ত ডেটা আগে থেকে ফেচ করার পরিবর্তে, Fetch-As-You-Render মানে আপনি ডেটা ফেচ করা শুরু করবেন *যত তাড়াতাড়ি সম্ভব*, প্রায়শই রেন্ডারিং প্রক্রিয়ার *আগে* বা *একই সাথে*। কম্পোনেন্টগুলো তখন একটি ক্যাশে থেকে ডেটা 'পড়ে', এবং যদি ডেটা প্রস্তুত না থাকে, তবে তারা সাসপেন্ড করে। মূল ধারণা হল ডেটা ফেচিং লজিককে কম্পোনেন্টের রেন্ডারিং লজিক থেকে আলাদা করা।

Fetch-As-You-Render বাস্তবায়ন করতে, আপনার একটি ব্যবস্থার প্রয়োজন হবে:

  1. কম্পোনেন্টের রেন্ডার ফাংশনের বাইরে একটি ডেটা ফেচ শুরু করা (যেমন, একটি রুটে প্রবেশ করার সময়, বা একটি বোতাম ক্লিক করা হলে)।
  2. প্রমিস বা রিজলভড ডেটা একটি ক্যাশে সংরক্ষণ করা।
  3. কম্পোনেন্টদের জন্য এই ক্যাশে থেকে 'পড়ার' একটি উপায় প্রদান করা। যদি ডেটা এখনও উপলব্ধ না থাকে, রিড ফাংশনটি পেন্ডিং প্রমিসটি থ্রো করে।

এই প্যাটার্নটি ওয়াটারফল সমস্যার সমাধান করে। যদি দুটি ভিন্ন কম্পোনেন্টের ডেটার প্রয়োজন হয়, তবে তাদের অনুরোধগুলো সমান্তরালভাবে শুরু করা যেতে পারে, এবং UI শুধুমাত্র তখনই প্রদর্শিত হবে যখন *উভয়ই* প্রস্তুত হবে, যা একটি একক Suspense বাউন্ডারি দ্বারা সমন্বিত হয়।

ম্যানুয়াল ইমপ্লিমেন্টেশন (বোঝার জন্য)

এর অন্তর্নিহিত প্রক্রিয়াটি বোঝার জন্য, আসুন একটি সরলীকৃত ম্যানুয়াল রিসোর্স ম্যানেজার তৈরি করি। একটি বাস্তব অ্যাপ্লিকেশনে, আপনি একটি ডেডিকেটেড লাইব্রেরি ব্যবহার করবেন।

import React, { Suspense } from 'react';

// --- সরল ক্যাশে/রিসোর্স ম্যানেজার --- //
const cache = new Map();

function createResource(promise) {
  let status = 'pending';
  let result;
  let suspender = promise.then(
    (r) => {
      status = 'success';
      result = r;
    },
    (e) => {
      status = 'error';
      result = e;
    }
  );

  return {
    read() {
      if (status === 'pending') {
        throw suspender;
      } else if (status === 'error') {
        throw result;
      } else if (status === 'success') {
        return result;
      }
    },
  };
}

function fetchData(key, fetcher) {
  if (!cache.has(key)) {
    cache.set(key, createResource(fetcher()));
  }
  return cache.get(key);
}

// --- ডেটা ফেচিং ফাংশন --- //
const fetchUserById = (id) => {
  console.log(`Fetching user ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const users = {
      '1': { id: '1', name: 'Alice Smith', email: 'alice@example.com' },
      '2': { id: '2', name: 'Bob Johnson', email: 'bob@example.com' },
      '3': { id: '3', name: 'Charlie Brown', email: 'charlie@example.com' }
    };
    resolve(users[id]);
  }, 1500));
};

const fetchPostsByUserId = (userId) => {
  console.log(`Fetching posts for user ${userId}...`);
  return new Promise(resolve => setTimeout(() => {
    const posts = {
      '1': [{ id: 'p1', title: 'My First Post' }, { id: 'p2', title: 'Travel Adventures' }],
      '2': [{ id: 'p3', title: 'Coding Insights' }],
      '3': [{ id: 'p4', title: 'Global Trends' }, { id: 'p5', title: 'Local Cuisine' }]
    };
    resolve(posts[userId] || []);
  }, 2000));
};

// --- কম্পোনেন্টস --- //
function UserProfile({ userId }) {
  const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  const user = userResource.read(); // ব্যবহারকারীর ডেটা প্রস্তুত না থাকলে এটি সাসপেন্ড করবে

  return (
    <div>
      <h3>User: {user.name}</h3>
      <p>Email: {user.email}</p>
    </div>
  );
}

function UserPosts({ userId }) {
  const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
  const posts = postsResource.read(); // পোস্টের ডেটা প্রস্তুত না থাকলে এটি সাসপেন্ড করবে

  return (
    <div>
      <h4>Posts by {userId}:</h4>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
        {posts.length === 0 && <li>No posts found.</li>}
      </ul>
    </div>
  );
}

// --- অ্যাপ্লিকেশন --- //
let initialUserResource = null;
let initialPostsResource = null;

function prefetchDataForUser(userId) {
  initialUserResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  initialPostsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
}

// App কম্পোনেন্ট রেন্ডার হওয়ার আগেই কিছু ডেটা প্রি-ফেচ করুন
prefetchDataForUser('1');

function App() {
  return (
    <div>
      <h1>Fetch-As-You-Render with Suspense</h1>
      <p>This demonstrates how data fetching can happen in parallel, coordinated by Suspense.</p>

      <Suspense fallback={<div>Loading user profile and posts...</div>}>
        <UserProfile userId={"1"} />
        <UserPosts userId={"1"} />
      </Suspense>

      <h2>Another Section</h2>
      <Suspense fallback={<div>Loading different user...</div>}>
        <UserProfile userId={"2"} />
      </Suspense>
    </div>
  );
}

এই উদাহরণে:

Fetch-As-You-Render-এর জন্য লাইব্রেরি

ম্যানুয়ালি একটি শক্তিশালী রিসোর্স ম্যানেজার তৈরি করা এবং রক্ষণাবেক্ষণ করা জটিল। সৌভাগ্যবশত, বেশ কয়েকটি পরিপক্ক ডেটা ফেচিং লাইব্রেরি Suspense গ্রহণ করেছে বা গ্রহণ করছে, যা যুদ্ধ-পরীক্ষিত সমাধান প্রদান করে:

এই লাইব্রেরিগুলো রিসোর্স তৈরি ও পরিচালনার জটিলতা, ক্যাশিং, রিভ্যালিডেশন, অপটিমিস্টিক আপডেট এবং এরর হ্যান্ডলিংয়ের মতো বিষয়গুলো থেকে মুক্তি দেয়, যা Fetch-As-You-Render বাস্তবায়নকে অনেক সহজ করে তোলে।

প্যাটার্ন ৪: Suspense-সচেতন লাইব্রেরিগুলির সাথে প্রিফেচিং

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

Suspense-সচেতন লাইব্রেরিগুলির সাথে, প্রিফেচিং নির্বিঘ্ন হয়ে ওঠে। আপনি ব্যবহারকারীর এমন ইন্টারঅ্যাকশনে ডেটা ফেচ ট্রিগার করতে পারেন যা অবিলম্বে UI পরিবর্তন করে না, যেমন একটি লিঙ্কের উপর হোভার করা বা একটি বোতামের উপর মাউস নিয়ে যাওয়া।

import React, { Suspense } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

// ধরুন এগুলো আপনার API কল
const fetchProductById = async (id) => {
  console.log(`Fetching product ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const products = {
      'A001': { id: 'A001', name: 'Global Widget X', price: 29.99, description: 'A versatile widget for international use.' },
      'B002': { id: 'B002', name: 'Universal Gadget Y', price: 149.99, description: 'Cutting-edge gadget, loved worldwide.' },
    };
    resolve(products[id]);
  }, 1000));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true, // ডিফল্টরূপে সমস্ত কোয়েরির জন্য Suspense সক্ষম করুন
    },
  },
});

function ProductDetails({ productId }) {
  const { data: product } = useQuery({
    queryKey: ['product', productId],
    queryFn: () => fetchProductById(productId),
  });

  return (
    <div style={{"border": "1px solid #ccc", "padding": "15px", "margin": "10px 0"}}>
      <h3>{product.name}</h3>
      <p>Price: ${product.price.toFixed(2)}</p>
      <p>{product.description}</p>
    </div>
  );
}

function ProductList() {
  const handleProductHover = (productId) => {
    // ব্যবহারকারী একটি প্রোডাক্ট লিঙ্কের উপর হোভার করলে ডেটা প্রিফেচ করুন
    queryClient.prefetchQuery({
      queryKey: ['product', productId],
      queryFn: () => fetchProductById(productId),
    });
    console.log(`Prefetching product ${productId}`);
  };

  return (
    <div>
      <h2>Available Products:</h2>
      <ul>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('A001')}
             onClick={(e) => { e.preventDefault(); /* নেভিগেট করুন বা বিবরণ দেখান */ }}
          >Global Widget X (A001)</a>
        </li>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('B002')}
             onClick={(e) => { e.preventDefault(); /* নেভিগেট করুন বা বিবরণ দেখান */ }}
          >Universal Gadget Y (B002)</a>
        </li>
      </ul>
      <p>Hover over a product link to see prefetching in action. Open network tab to observe.</p>
    </div>
  );
}

function App() {
  const [showProductA, setShowProductA] = React.useState(false);
  const [showProductB, setShowProductB] = React.useState(false);

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Prefetching with React Suspense (React Query)</h1>
      <ProductList />

      <button onClick={() => setShowProductA(true)}>Show Global Widget X</button>
      <button onClick={() => setShowProductB(true)}>Show Universal Gadget Y</button>

      {showProductA && (
        <Suspense fallback={<p>Loading Global Widget X...</p>}>
          <ProductDetails productId="A001" />
        </Suspense>
      )}
      {showProductB && (
        <Suspense fallback={<p>Loading Universal Gadget Y...</p>}>
          <ProductDetails productId="B002" />
        </Suspense>
      )}
    </QueryClientProvider>
  );
}

এই উদাহরণে, একটি প্রোডাক্ট লিঙ্কের উপর হোভার করা `queryClient.prefetchQuery` ট্রিগার করে, যা পটভূমিতে ডেটা ফেচ শুরু করে। যদি ব্যবহারকারী তখন প্রোডাক্টের বিবরণ দেখানোর জন্য বোতামটি ক্লিক করে, এবং প্রিফেচ থেকে ডেটা ইতিমধ্যে ক্যাশে থাকে, তবে কম্পোনেন্টটি সাসপেন্ড না করেই সঙ্গে সঙ্গে রেন্ডার হবে। যদি প্রিফেচ এখনও চলছে বা শুরু না হয়ে থাকে, Suspense ডেটা প্রস্তুত না হওয়া পর্যন্ত ফলব্যাক প্রদর্শন করবে।

Suspense এবং Error Boundaries দিয়ে এরর হ্যান্ডলিং

যদিও Suspense একটি ফলব্যাক প্রদর্শন করে 'লোডিং' স্টেট পরিচালনা করে, এটি সরাসরি 'এরর' স্টেট পরিচালনা করে না। যদি একটি সাসপেন্ডিং কম্পোনেন্ট দ্বারা থ্রো করা প্রমিসটি রিজেক্ট হয় (অর্থাৎ, ডেটা ফেচিং ব্যর্থ হয়), এই এররটি কম্পোনেন্ট ট্রি-এর উপরে প্রচারিত হবে। এই এররগুলোকে সুন্দরভাবে পরিচালনা করতে এবং একটি উপযুক্ত UI প্রদর্শন করতে, আপনাকে Error Boundaries ব্যবহার করতে হবে।

একটি এরর বাউন্ডারি হল একটি React কম্পোনেন্ট যা হয় componentDidCatch অথবা static getDerivedStateFromError লাইফসাইকেল মেথড প্রয়োগ করে। এটি তার চাইল্ড কম্পোনেন্ট ট্রি-এর যেকোনো জায়গায় জাভাস্ক্রিপ্ট এরর ধরে, যার মধ্যে এমন প্রমিস দ্বারা থ্রো করা এররও অন্তর্ভুক্ত যা Suspense সাধারণত ধরত যদি তারা পেন্ডিং থাকত।

import React, { Suspense, useState } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

// --- এরর বাউন্ডারি কম্পোনেন্ট --- //
class MyErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // স্টেট আপডেট করুন যাতে পরবর্তী রেন্ডার ফলব্যাক UI দেখাবে।
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // আপনি একটি এরর রিপোর্টিং সার্ভিসে এররটি লগ করতে পারেন
    console.error("Caught an error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // আপনি যেকোনো কাস্টম ফলব্যাক UI রেন্ডার করতে পারেন
      return (
        <div style={{"border": "2px solid red", "padding": "20px", "margin": "20px 0", "background": "#ffe0e0"}}>
          <h2>Something went wrong!</h2>
          <p>{this.state.error && this.state.error.message}</p>
          <p>Please try refreshing the page or contact support.</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>Try Again</button>
        </div>
      );
    }
    return this.props.children;
  }
}

// --- ডেটা ফেচিং (এররের সম্ভাবনা সহ) --- //
const fetchItemById = async (id) => {
  console.log(`Attempting to fetch item ${id}...`);
  return new Promise((resolve, reject) => setTimeout(() => {
    if (id === 'error-item') {
      reject(new Error('Failed to load item: Network unreachable or item not found.'));
    } else if (id === 'slow-item') {
      resolve({ id: 'slow-item', name: 'Delivered Slowly', data: 'This item took a while but arrived!', status: 'success' });
    } else {
      resolve({ id, name: `Item ${id}`, data: `Data for item ${id}` });
    }
  }, id === 'slow-item' ? 3000 : 800));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
      retry: false, // প্রদর্শনের জন্য, রিট্রাই নিষ্ক্রিয় করুন যাতে এররটি তাৎক্ষণিক হয়
    },
  },
});

function DisplayItem({ itemId }) {
  const { data: item } = useQuery({
    queryKey: ['item', itemId],
    queryFn: () => fetchItemById(itemId),
  });

  return (
    <div>
      <h3>Item Details:</h3>
      <p>ID: {item.id}</p>
      <p>Name: {item.name}</p>
      <p>Data: {item.data}</p>
    </div>
  );
}

function App() {
  const [fetchType, setFetchType] = useState('normal-item');

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Suspense and Error Boundaries</h1>

      <div>
        <button onClick={() => setFetchType('normal-item')}>Fetch Normal Item</button>
        <button onClick={() => setFetchType('slow-item')}>Fetch Slow Item</button>
        <button onClick={() => setFetchType('error-item')}>Fetch Error Item</button>
      </div>

      <MyErrorBoundary>
        <Suspense fallback={<p>Loading item via Suspense...</p>}>
          <DisplayItem itemId={fetchType} />
        </Suspense>
      </MyErrorBoundary>
    </QueryClientProvider>
  );
}

আপনার Suspense বাউন্ডারি (অথবা যে কম্পোনেন্টগুলো সাসপেন্ড হতে পারে) একটি এরর বাউন্ডারি দিয়ে মুড়ে দিলে, আপনি নিশ্চিত করতে পারেন যে ডেটা ফেচিংয়ের সময় নেটওয়ার্ক ব্যর্থতা বা সার্ভার এররগুলো ধরা পড়বে এবং সুন্দরভাবে পরিচালনা করা হবে, যা পুরো অ্যাপ্লিকেশনটিকে ক্র্যাশ করা থেকে রক্ষা করবে। এটি একটি শক্তিশালী এবং ব্যবহারকারী-বান্ধব অভিজ্ঞতা প্রদান করে, যা ব্যবহারকারীদের সমস্যাটি বুঝতে এবং সম্ভাব্যভাবে পুনরায় চেষ্টা করতে দেয়।

Suspense-এর সাথে স্টেট ম্যানেজমেন্ট এবং ডেটা ইনভ্যালিডেশন

এটি স্পষ্ট করা গুরুত্বপূর্ণ যে React Suspense প্রাথমিকভাবে অ্যাসিঙ্ক্রোনাস রিসোর্সের প্রাথমিক লোডিং স্টেট পরিচালনা করে। এটি অন্তর্নিহিতভাবে ক্লায়েন্ট-সাইড ক্যাশে পরিচালনা করে না, ডেটা ইনভ্যালিডেশন সামলায় না, বা মিউটেশন (ক্রিয়েট, আপডেট, ডিলিট অপারেশন) এবং তাদের পরবর্তী UI আপডেটগুলো সমন্বয় করে না।

এখানেই Suspense-সচেতন ডেটা ফেচিং লাইব্রেরিগুলো (React Query, SWR, Apollo Client, Relay) অপরিহার্য হয়ে ওঠে। তারা Suspense-কে পরিপূরক করে প্রদান করে:

একটি শক্তিশালী ডেটা ফেচিং লাইব্রেরি ছাড়া, একটি ম্যানুয়াল Suspense রিসোর্স ম্যানেজারের উপরে এই বৈশিষ্ট্যগুলো বাস্তবায়ন করা একটি উল্লেখযোগ্য উদ্যোগ হবে, যা মূলত আপনার নিজের ডেটা ফেচিং ফ্রেমওয়ার্ক তৈরি করার সমতুল্য।

ব্যবহারিক বিবেচনা এবং সেরা অনুশীলন

ডেটা ফেচিংয়ের জন্য Suspense গ্রহণ করা একটি গুরুত্বপূর্ণ স্থাপত্যিক সিদ্ধান্ত। একটি গ্লোবাল অ্যাপ্লিকেশনের জন্য এখানে কিছু ব্যবহারিক বিবেচনা রয়েছে:

১. সব ডেটার জন্য Suspense প্রয়োজন নেই

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

২. Suspense বাউন্ডারির গ্র্যানুলারিটি

আপনার <Suspense> বাউন্ডারিগুলো চিন্তাভাবনা করে স্থাপন করুন। আপনার অ্যাপ্লিকেশনের শীর্ষে একটি একক, বড় বাউন্ডারি পুরো পৃষ্ঠাটিকে একটি স্পিনারের আড়ালে লুকিয়ে ফেলতে পারে, যা হতাশাজনক হতে পারে। ছোট, আরও দানাদার বাউন্ডারিগুলো আপনার পৃষ্ঠার বিভিন্ন অংশকে স্বাধীনভাবে লোড করতে দেয়, যা একটি আরও প্রগতিশীল এবং প্রতিক্রিয়াশীল অভিজ্ঞতা প্রদান করে। উদাহরণস্বরূপ, একটি ব্যবহারকারী প্রোফাইল কম্পোনেন্টের চারপাশে একটি বাউন্ডারি, এবং প্রস্তাবিত পণ্যগুলির একটি তালিকার চারপাশে আরেকটি।

<div>
  <h1>Product Page</h1>
  <Suspense fallback={<p>Loading main product details...</p>}>
    <ProductDetails id="prod123" />
  </Suspense>

  <hr />

  <h2>Related Products</h2>
  <Suspense fallback={<p>Loading related products...</p>}>
    <RelatedProducts category="electronics" />
  </Suspense>
</div>

এই পদ্ধতিটির মানে হল ব্যবহারকারীরা মূল পণ্যের বিবরণ দেখতে পারে এমনকি যদি সম্পর্কিত পণ্যগুলো এখনও লোড হচ্ছে।

৩. সার্ভার-সাইড রেন্ডারিং (SSR) এবং স্ট্রিমিং HTML

React 18-এর নতুন স্ট্রিমিং SSR API (renderToPipeableStream) সম্পূর্ণরূপে Suspense-এর সাথে সংহত। এটি আপনার সার্ভারকে HTML প্রস্তুত হওয়ার সাথে সাথেই পাঠাতে দেয়, এমনকি যদি পৃষ্ঠার কিছু অংশ (যেমন ডেটা-নির্ভর কম্পোনেন্ট) এখনও লোড হচ্ছে। সার্ভার একটি প্লেসহোল্ডার (Suspense ফলব্যাক থেকে) স্ট্রিম করতে পারে এবং তারপর ডেটা রিজলভ হলে আসল কন্টেন্ট স্ট্রিম করতে পারে, ক্লায়েন্ট-সাইডে সম্পূর্ণ পুনরায় রেন্ডারের প্রয়োজন ছাড়াই। এটি বিভিন্ন নেটওয়ার্ক পরিস্থিতিতে গ্লোবাল ব্যবহারকারীদের জন্য অনুভূত লোডিং পারফরম্যান্সকে উল্লেখযোগ্যভাবে উন্নত করে।

৪. পর্যায়ক্রমিক গ্রহণ

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

৫. টুলিং এবং ডিবাগিং

যদিও Suspense কম্পোনেন্ট লজিককে সহজ করে, ডিবাগিং ভিন্ন হতে পারে। React DevTools Suspense বাউন্ডারি এবং তাদের অবস্থা সম্পর্কে অন্তর্দৃষ্টি প্রদান করে। আপনার নির্বাচিত ডেটা ফেচিং লাইব্রেরি কীভাবে তার অভ্যন্তরীণ অবস্থা প্রকাশ করে (যেমন, React Query Devtools) সে সম্পর্কে পরিচিত হন।

৬. Suspense ফলব্যাকের জন্য টাইমআউট

খুব দীর্ঘ লোডিং সময়ের জন্য, আপনি আপনার Suspense ফলব্যাকে একটি টাইমআউট যোগ করতে চাইতে পারেন, অথবা একটি নির্দিষ্ট বিলম্বের পরে আরও বিস্তারিত লোডিং ইন্ডিকেটরে স্যুইচ করতে পারেন। React 18-এর useDeferredValue এবং useTransition হুকগুলো এই আরও সূক্ষ্ম লোডিং স্টেটগুলো পরিচালনা করতে সাহায্য করতে পারে, যা আপনাকে নতুন ডেটা ফেচ করার সময় UI-এর একটি 'পুরানো' সংস্করণ দেখাতে, বা অ-জরুরী আপডেটগুলো স্থগিত করতে দেয়।

React-এ ডেটা ফেচিং-এর ভবিষ্যৎ: React সার্ভার কম্পোনেন্ট এবং তার পরেও

React-এ ডেটা ফেচিংয়ের যাত্রা ক্লায়েন্ট-সাইড Suspense-এ শেষ হয় না। React সার্ভার কম্পোনেন্টস (RSC) একটি উল্লেখযোগ্য বিবর্তনকে প্রতিনিধিত্ব করে, যা ক্লায়েন্ট এবং সার্ভারের মধ্যেকার সীমানা ঝাপসা করে দেওয়ার এবং ডেটা ফেচিংকে আরও অপ্টিমাইজ করার প্রতিশ্রুতি দেয়।

React পরিপক্ক হওয়ার সাথে সাথে, Suspense অত্যন্ত পারফরম্যান্ট, ব্যবহারকারী-বান্ধব এবং রক্ষণাবেক্ষণযোগ্য অ্যাপ্লিকেশন তৈরির জন্য পাজলের একটি ক্রমবর্ধমান কেন্দ্রীয় অংশ হবে। এটি ডেভেলপারদের অ্যাসিঙ্ক্রোনাস অপারেশনগুলো পরিচালনা করার জন্য একটি আরও ডিক্লেয়ারেটিভ এবং স্থিতিস্থাপক উপায়ের দিকে ঠেলে দেয়, যা জটিলতাকে পৃথক কম্পোনেন্ট থেকে একটি সু-পরিচালিত ডেটা লেয়ারে স্থানান্তরিত করে।

উপসংহার

React Suspense, প্রাথমিকভাবে কোড স্প্লিটিংয়ের জন্য একটি বৈশিষ্ট্য, ডেটা ফেচিংয়ের জন্য একটি রূপান্তরমূলক টুলে পরিণত হয়েছে। Fetch-As-You-Render প্যাটার্ন গ্রহণ করে এবং Suspense-সচেতন লাইব্রেরিগুলো ব্যবহার করে, ডেভেলপাররা তাদের অ্যাপ্লিকেশনগুলোর ব্যবহারকারীর অভিজ্ঞতা উল্লেখযোগ্যভাবে উন্নত করতে পারে, লোডিং ওয়াটারফল দূর করতে পারে, কম্পোনেন্ট লজিক সহজ করতে পারে এবং মসৃণ, সমন্বিত লোডিং স্টেট সরবরাহ করতে পারে। শক্তিশালী এরর হ্যান্ডলিংয়ের জন্য এরর বাউন্ডারির সাথে এবং React সার্ভার কম্পোনেন্টসের ভবিষ্যতের প্রতিশ্রুতির সাথে মিলিত হয়ে, Suspense আমাদের এমন অ্যাপ্লিকেশন তৈরি করতে ক্ষমতা দেয় যা কেবল পারফরম্যান্ট এবং স্থিতিস্থাপকই নয়, বিশ্বজুড়ে ব্যবহারকারীদের জন্য সহজাতভাবে আরও আনন্দদায়ক। Suspense-চালিত ডেটা ফেচিং প্যারাডাইমে স্থানান্তর একটি ধারণাগত সমন্বয়ের প্রয়োজন, তবে কোডের স্বচ্ছতা, পারফরম্যান্স এবং ব্যবহারকারীর সন্তুষ্টির ক্ষেত্রে সুবিধাগুলো যথেষ্ট এবং বিনিয়োগের যোগ্য।