<Suspense>
<Suspense> তার children এর লোডিং শেষ না হওয়া পর্যন্ত একটি fallback প্রদর্শন করে।
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>- রেফারেন্স
- ব্যবহারবিধি
- কন্টেন্ট লোড হওয়ার সময় একটি fallback দেখানো
- সামগ্রিকভাবে একসাথে কন্টেন্ট প্রকাশ করা
- নেস্টেড কন্টেন্ট লোড হওয়ার সাথে সাথে প্রকাশ করা
- নতুন কনটেন্ট লোড হতে হতে পুরাতন কন্টেন্ট দেখানো
- প্রকাশ করে ফেলা কন্টেন্টগুলো লুকাতে না দেয়া
- ট্রান্সিশন ঘটছে এটি ইন্ডিকেট করা
- নেভিগেশনে Suspense boundary গুলো রিসেট করা
- Server errors এবং client-only কন্টেন্টের জন্য একটি fallback দেওয়া
- শুধু ব্রাউজারে রেন্ডার করা কনটেন্টের জন্য fallback দেখানো
- stylesheet লোড হওয়ার জন্য অপেক্ষা করা
- Suspense কনটেন্ট থেকে অ্যানিমেশন চালানো
- ফন্ট লোড হওয়ার জন্য অপেক্ষা করা
- ছবি লোড হওয়ার জন্য অপেক্ষা করা
- ফন্ট, ছবি ও stylesheet-এর লোডিং সমন্বয় করা
- সমস্যার সমাধান করা
রেফারেন্স
<Suspense>
প্রপ্স
children: আপনি যেই প্রকৃত UI রেন্ডার করতে চাইছেন।childrenযদি রেন্ডারিং এর সময় থেমে যায় তাহলে Suspense boundaryfallbackরেন্ডার করবে।fallback: এটি হচ্ছে, প্রকৃত UI পুরোপুরি লোড না হলে তার স্থানে রেন্ডার হওয়া একটি বিকল্প UI। যেকোনো বৈধ React নোডই গৃহীত হয়, যদিও কার্যত, একটি fallback হলো একটি হালকা placeholder view, যেমন একটি লোডিং স্পিনার বা স্কেলেটন। Suspense স্বয়ংক্রিয় ভাবেfallbackএ পরিবর্তিত হয়ে যাবে যখনchildrenথেমে যাবে এবং আবারchildrenএ ফিরে আসবে যখন ডেটা তৈরি হয়ে যাবে। রেন্ডারিং এর সময় যদিfallbackথেমে যায়, এটি তার নিকটতম প্যারেন্ট Suspense boundary সক্রিয় করবে।- Experimental only অপশনাল
defer: একটি boolean।trueহলে React প্রথমেfallbackদেখিয়ে পরেchildrenরেন্ডার বা stream করতে পারে, এমনকি তাদের কোনো কিছু suspend না করলেও। রেন্ডার করতে বেশি কাজ লাগে এমন কনটেন্টের জন্য এটি ব্যবহার করুন। ডিফল্ট মানfalse।
সতর্কতা
- Effect বা event handler-এর ভেতরে ডেটা fetch করা হলে Suspense তা শনাক্ত করে না। এটি শুধু নিচে দেওয়া ক্ষেত্রগুলোতে সক্রিয় হয়।
- প্রথমবার মাউন্ট হওয়ার আগেই suspend হয়ে যাওয়া রেন্ডারের কোনো state React সংরক্ষণ করে না। কম্পোনেন্ট লোড হলে React সেই ট্রি শুরু থেকে আবার রেন্ডার করার চেষ্টা করে।
- Suspense কোনো ট্রির কনটেন্ট দেখানোর পরে সেটি আবার suspend হলে
fallbackআবার দেখানো হবে, যদি না সংশ্লিষ্ট আপডেটটিstartTransitionবাuseDeferredValueদিয়ে করা হয়ে থাকে। - React সর্বশেষ কনটেন্ট প্রকাশের সময় থেকে প্রতি 300 মিলিসেকেন্ডে সর্বোচ্চ একবার suspend হওয়া কনটেন্ট প্রকাশ করে। এই সময়ের মধ্যে প্রস্তুত হওয়া বাউন্ডারিগুলো একটির পর একটি না দেখিয়ে একসাথে প্রকাশ করা হয়।
- আবার suspend হওয়ার কারণে আগে দেখানো কনটেন্ট লুকাতে হলে, React কনটেন্ট ট্রির layout Effect-গুলোর cleanup চালায়। কনটেন্ট আবার দেখানোর জন্য প্রস্তুত হলে React layout Effect-গুলো পুনরায় চালায়। এতে DOM layout মাপার Effect-গুলো কনটেন্ট লুকানো অবস্থায় তা মাপার চেষ্টা করে না।
- React-এর Streaming Server Rendering এবং Selective Hydration-এর মতো অভ্যন্তরীণ অপটিমাইজেশনগুলো Suspense-এর সাথে সমন্বিত। আরও জানতে আর্কিটেকচারের সারমর্ম পড়ুন এবং টেকনিক্যাল আলোচনা দেখুন।
কী কী কারণে একটি Suspense বাউন্ডারি সক্রিয় হয়
একটি Suspense বাউন্ডারি তার কনটেন্ট প্রস্তুত হওয়া পর্যন্ত সেটি প্রকাশের জন্য অপেক্ষা করে। নিচের যেকোনো কারণে বাউন্ডারির কনটেন্ট প্রকাশ আটকে থাকতে পারে:
lazyদিয়ে কম্পোনেন্টের কোড লোড করা।useদিয়ে একটি Promise পড়া। এর মধ্যে Server Component থেকে stream করা ডেটা বা Suspense সমর্থিত ফ্রেমওয়ার্ক দিয়ে লোড করা ডেটাও রয়েছে।<link rel="stylesheet">এবংprecedenceprop দিয়ে রেন্ডার করা stylesheet লোড করা। একটি নির্দিষ্ট সময়সীমা পর্যন্ত, stylesheet লোড না হওয়া অবধি React বাউন্ডারির কনটেন্ট প্রকাশ আটকে রাখে। নিচের উদাহরণ দেখুন।- Streaming server rendering-এর সময় বড় কোনো বাউন্ডারির HTML এসে পৌঁছানোর জন্য অপেক্ষা করা। HTML পাঠাতে সময় লাগে, তাই যথেষ্ট পরিমাণ কনটেন্ট থাকলে তার ভেতরে কিছু suspend না হলেও বাউন্ডারি সক্রিয় হয়। HTML পৌঁছানোর সাথে সাথে React কনটেন্ট প্রকাশ করে।
- ফন্ট লোড করা। সাধারণভাবে Suspense ফন্টের জন্য অপেক্ষা করে না। তবে
<ViewTransition>দিয়ে বাউন্ডারির কনটেন্ট প্রকাশের অ্যানিমেশন চালালে, React নতুন ফন্ট লোড হওয়ার জন্য সর্বোচ্চ 500 মিলিসেকেন্ড অপেক্ষা করে, যেন টেক্সট হঠাৎ fallback ফন্টে দেখা না যায়। নিচের উদাহরণ দেখুন। - ছবি লোড করা। সাধারণভাবে Suspense ছবির জন্য অপেক্ষা করে না। তবে
<ViewTransition>দিয়ে বাউন্ডারির কনটেন্ট প্রকাশের অ্যানিমেশন চালালে, অ্যানিমেশন শুরুর আগে React দৃশ্যমান ছবিগুলো লোড হওয়ার জন্য সর্বোচ্চ 500 মিলিসেকেন্ড অপেক্ষা করে। কোনো ছবিতেloading="lazy"বাonLoadhandler দিলে সেই ছবির জন্য অপেক্ষা করা হয় না। নিচের উদাহরণ দেখুন। - Experimental only
<Suspense defer>বাউন্ডারির ভেতরে CPU-নির্ভর রেন্ডারিংয়ের কাজ করা।
ব্যবহারবিধি
কন্টেন্ট লোড হওয়ার সময় একটি fallback দেখানো
আপনি আপনার অ্যাপ্লিকেশনের যে কোন অংশকে একটি Suspense boundary দিয়ে আবদ্ধ করতে পারেনঃ
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>Children এর প্রয়োজনীয় সমস্ত কোড এবং ডেটা লোড না হওয়া পর্যন্ত React আপনার লোডিং fallback দেখাবে।
নিচের উদাহরণে, Albums কম্পোনেন্ট একটি অ্যালবাম তালিকা নিয়ে আসার সময় থেমে যায়। রেন্ডারের জন্য প্রস্তুত হওয়া পর্যন্ত, React উপরের সবচেয়ে কাছের Suspense boundary ব্যবহার করে fallback হিসেবে আপনার লোডিং কম্পোনেন্ত দেখায়। তারপরে, ডেটা লোড হলে, React লোডিং fallback লুকিয়ে দেয় এবং ডেটা সহ Albums কম্পোনেন্টটি রেন্ডার করে।
import { Suspense } from 'react'; import Albums from './Albums.js'; export default function ArtistPage({ artist }) { return ( <> <h1>{artist.name}</h1> <Suspense fallback={<Loading />}> <Albums artistId={artist.id} /> </Suspense> </> ); } function Loading() { return <h2>🌀 Loading...</h2>; }
অন্যদিকে, use ছাড়া অন্য কোনোভাবে—যেমন Effect-এর ভেতরে—ডেটা fetch করলে বাউন্ডারিটি সক্রিয় হয় না:
import { Suspense } from 'react'; import EffectAlbums from './EffectAlbums.js'; export default function ArtistPage({ artist }) { return ( <> <h1>{artist.name}</h1> <Suspense fallback={<Loading />}> <EffectAlbums artistId={artist.id} /> </Suspense> </> ); } function Loading() { return <h2>🌀 Loading...</h2>; }
During streaming server rendering, a boundary also activates while its HTML is still streaming in. With any streaming server rendering API, React sends the shell with the fallback first, then streams in each boundary’s HTML and swaps out its fallback as that content arrives. Press “Render the page” to watch the page stream in:
import { flushReadableStreamToFrame } from './demo-helpers.js'; import { Suspense, use } from 'react'; import { renderToReadableStream } from 'react-dom/server'; let posts = null; function Posts() { const text = use(posts.promise); return <p>{text}</p>; } function ProfilePage() { return ( <html> <body> <h1>Alice</h1> <p>Photographer and traveler.</p> <Suspense fallback={<p>⌛ Loading posts...</p>}> <Posts /> </Suspense> </body> </html> ); } async function main(frame) { posts = Promise.withResolvers(); const stream = await renderToReadableStream(<ProfilePage />); // The posts resolve after the shell has streamed, so React // streams their HTML in and swaps out the fallback. setTimeout(() => { posts.resolve( 'Just got back from two weeks along the coast. The drive ' + 'was longer than expected, but every stop was worth it. ' + 'A full write-up and more photos are coming soon.' ); }, 1500); await flushReadableStreamToFrame(stream, frame); } document.getElementById('render').addEventListener('click', () => { main(document.getElementById('container')); });
সামগ্রিকভাবে একসাথে কন্টেন্ট প্রকাশ করা
স্বাভাবিকভাবে, Suspense এর ভিতরে অবস্থিত সম্পূর্ণ ট্রি একটি একক ইউনিট হিসাবে বিবেচিত হয়। উদাহরণস্বরূপ, যদিও এই কম্পোনেন্টগুলোর মধ্যে কেবল একটিই কিছু ডেটার জন্য অপেক্ষায় থাকে, সবগুলো কম্পোনেন্ট একসাথে লোডিং ইন্ডিকেটর দ্বারা প্রতিস্থাপিত হবেঃ
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>তারপরে, সবগুলো দেখনোর জন্য প্রস্তুত হয়ে গেলে, তাদের সবাইকে একসাথে দেখা যাবে।
নিচের উদাহরণে, Biography এবং Albums উভয়ই কিছু ডেটা নিয়ে আসে। তবে, তারা একটি একক Suspense boundary এর মধ্যে গ্রুপ করে থাকায় সবসময় এই কম্পোনেন্টগুলি একই সময়ে “পপ ইন” করে।
import { Suspense } from 'react'; import Albums from './Albums.js'; import Biography from './Biography.js'; import Panel from './Panel.js'; export default function ArtistPage({ artist }) { return ( <> <h1>{artist.name}</h1> <Suspense fallback={<Loading />}> <Biography artistId={artist.id} /> <Panel> <Albums artistId={artist.id} /> </Panel> </Suspense> </> ); } function Loading() { return <h2>🌀 Loading...</h2>; }
যে কম্পোনেন্টগুলো ডেটা লোড করে তাদেরকে Suspense boundary এর direct children হতে হবে এমনটি নয়। উদাহরণস্বরূপ, আপনি Biography এবং Albums কে একটি নতুন Details কম্পোনেন্টে সরিয়ে নিতে পারেন। এটা আচরণের পরিবর্তন করে না। Biography এবং Albums এর সবচেয়ে কাছের প্যারেন্ট Suspense boundary একই, তাই তাদের প্রকাশ এক সঙ্গে সমন্বিতভাবে হয়।
<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>
function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}নেস্টেড কন্টেন্ট লোড হওয়ার সাথে সাথে প্রকাশ করা
যখন একটি কম্পোনেন্ট থেমে যায়, তখন সবচেয়ে কাছের প্যারেন্ট Suspense কম্পোনেন্ট fallback দেখায়। এর মাধ্যমে আপনি একাধিক Suspense কম্পোনেন্ট দ্বারা একটি লোডিং ক্রম তৈরি করতে পারেন। প্রতিটি Suspense boundary এর fallback পূর্ণ হবে যদি পরবর্তী স্তরের কন্টেন্ট থাকে। উদাহরণস্বরূপ, আপনি অ্যালবাম তালিকায় এর নিজস্ব fallback দিতে পারেনঃ
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>এই পরিবর্তনের ফলে Biography প্রদর্শনে Albums এর লোডের জন্য অপেক্ষা করার প্রয়োজন নেই।
ক্রমটি হবে এরকমঃ
১। যদি Biography এখনো লোড না হয়ে থাকে, কন্টেন্ট এর সম্পূর্ণ জায়গাজুড়ে BigSpinner দেখানো হয়।
২। Biography লোড করা শেষ হলে BigSpinner দ্বারা কন্টেন্ট প্রতিস্থাপিত হয়।
৩। যদি এখনো Albums লোড না হয়ে থাকে, AlbumsGlimmer এর স্থানে Albums এবং এর প্যারেন্ট Panel দেখানো হয়।
৪। সব শেষে, Albums এর লোডিং শেষ হলে এটি AlbumsGlimmer কে প্রতিস্থাপন করে।
import { Suspense } from 'react'; import Albums from './Albums.js'; import Biography from './Biography.js'; import Panel from './Panel.js'; export default function ArtistPage({ artist }) { return ( <> <h1>{artist.name}</h1> <Suspense fallback={<BigSpinner />}> <Biography artistId={artist.id} /> <Suspense fallback={<AlbumsGlimmer />}> <Panel> <Albums artistId={artist.id} /> </Panel> </Suspense> </Suspense> </> ); } function BigSpinner() { return <h2>🌀 Loading...</h2>; } function AlbumsGlimmer() { return ( <div className="glimmer-panel"> <div className="glimmer-line" /> <div className="glimmer-line" /> <div className="glimmer-line" /> </div> ); }
Suspense boundary এর মাধ্যমে আপনি আপনার UI এর কোন অংশগুলো সবসময় একই সময়ে “পপ ইন” করা হবে এবং কোন অংশগুলি লোডিং state এর একটি অনুক্রমিক সিকোয়েন্সে আরো কন্টেন্ট প্রকাশ করবে তা সমন্বিত করতে পারেন। আপনি ট্রির যে কোন জায়গায় Suspense boundaries যোগ করতে পারেন, সরাতে পারেন বা মুছতে পারেন কিন্তু অ্যাপ্লিকেশনের অন্যান্য অংশের আচরণের উপর এর কোন প্রভাব পড়বে না।
প্রত্যেকটি কম্পোনেন্টের সাথে একটি করে Suspense boundary যোগ করবেন না। Suspense boundary গুলো লোডিং ক্রমের চেয়ে ক্ষুদ্র হওয়া উচিৎ নয় যা ব্যহারকারীকে দেখাবেন। যদি আপনি একজন ডিজাইনার সঙ্গে কাজ করেন, তবে তাদেরকে জিজ্ঞাসা করুন যে লোডিং state গুলো কোথায় রাখা উচিত—সম্ভবতঃ তারা আগেই তা তাদের ডিজাইন ওয়্যারফ্রেমে অন্তর্ভুক্ত করেছেন।
নতুন কনটেন্ট লোড হতে হতে পুরাতন কন্টেন্ট দেখানো
এই উদাহরণে SearchResults কম্পোনেন্টটি সার্চের ফলাফল নিয়ে আসার সময় থেমে যায়। "a" টাইপ করে অপেক্ষা করুন, তারপর এটিকে এডিট করে "ab" করুন। "a" এর ফলাফলগুলো লোডিং fallback দ্বারা প্রতিস্থাপিত হয়ে যাবে।
import { Suspense, useState } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); return ( <> <label> Search albums: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <SearchResults query={query} /> </Suspense> </> ); }
একটি প্রচলিত বিকল্প UI প্যাটার্ন হলো তালিকা আপডেটকে বিলম্বিত করা(defer) এবং নতুন ফলাফল তৈরি হওয়া পর্যন্ত আগের ফলাফলগুলো দেখানো। useDeferredValue হুকটি আপনাকে query করার একটি deferred ভার্সন পাঠানোর সুবিধা দেয়ঃ
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}query টি তাৎক্ষণিকভাবে আপডেট হবে, তাই ইনপুটটি নতুন মানটি দেখাবে। তবে, deferredQuery ডেটা লোড না হওয়া পর্যন্ত তার আগের মানটি রেখে দিবে, তাই SearchResults কিছুক্ষণের জন্য পুরাতন ফলাফল দেখাবে।
ব্যবহারকারীর কাছে এটিকে আরও স্পষ্ট করতে, যখন পুরাতন ফলাফল তালিকা দেখানো হচ্ছে তখন একটি ভিজুয়াল ইন্ডিকেশন যোগ করতে পারেনঃ
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>নিচের উদাহরণে "a" ইনপুট দিন, ফলাফল লোড হওয়ার জন্য অপেক্ষা করুন এবং তারপর ইনপুটটি এডিট করে "ab" করুন। লক্ষ্য করুন যে নতুন ফলাফলগুলো লোড না হওয়া পর্যন্ত Suspense fallback এর পরিবর্তে আপনি এখন পুরাতন ফলাফল তালিকাটি কিছুটা অস্পষ্ট ভাবে দেখতে পাচ্ছেনঃ
import { Suspense, useState, useDeferredValue } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); const isStale = query !== deferredQuery; return ( <> <label> Search albums: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <div style={{ opacity: isStale ? 0.5 : 1 }}> <SearchResults query={deferredQuery} /> </div> </Suspense> </> ); }
প্রকাশ করে ফেলা কন্টেন্টগুলো লুকাতে না দেয়া
যখন একটি কম্পোনেন্ট থেমে যায় তখন সবচেয়ে কাছের প্যরেন্ট Suspense boundary এটির পরিবর্তে fallback দেখায়। এমন যদি হয় যে এটি আগে থেকেই কিছু কন্টেন্ট দেখাচ্ছিল, তবে এটি ব্যবহারকারীর জন্য একটি অস্বাভাবিক অভিজ্ঞতা সৃষ্টি করতে পারে। এই বাটনটি প্রেস করে দেখুনঃ
import { Suspense, useState } from 'react'; import IndexPage from './IndexPage.js'; import ArtistPage from './ArtistPage.js'; import Layout from './Layout.js'; export default function App() { return ( <Suspense fallback={<BigSpinner />}> <Router /> </Suspense> ); } function Router() { const [page, setPage] = useState('/'); function navigate(url) { setPage(url); } let content; if (page === '/') { content = ( <IndexPage navigate={navigate} /> ); } else if (page === '/the-beatles') { content = ( <ArtistPage artist={{ id: 'the-beatles', name: 'The Beatles', }} /> ); } return ( <Layout> {content} </Layout> ); } function BigSpinner() { return <h2>🌀 Loading...</h2>; }
আপনি যখন বাটনটি প্রেস করেছেন তখন Router কম্পোনেন্টটি IndexPage এর পরিবর্তে ArtistPage কে রেন্ডার করেছে। ArtistPage এর ভিতরের একটি কম্পোনেন্ট থেমে গেছে, তাই সবচেয়ে কাছের Suspense boundary fallback দেখানো শুরু করেছে। সবচেয়ে কাছের Suspense boundary রুটের কাছাকাছি ছিল, তাই সাইটের সমগ্র লেআউটটি BigSpinner দ্বারা প্রতিস্থাপিত হয়ে গেছে।
আপনি যদি চান যে এমনটি না হোক সেক্ষেত্রে আপনি startTransition দ্বারা navigation state update কে transition হিসেবে চিহ্নিত করে দিতে পারেনঃ
function Router() {
const [page, setPage] = useState('/');
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...এটা React কে বলে যে state ট্রান্সিশনটি এখনই জরুরী নয়, এবং ইতিমধ্যেই প্রকাশ হয়ে যাওয়া কোনো কন্টেন্ট লুকানোর চেয়ে আগের পেজটি দেখিয়ে রাখাই ভাল। এখন বাটনটি ক্লিক করা হলে এটি Biography লোড হওয়ার জন্য “অপেক্ষা” করবেঃ
import { Suspense, startTransition, useState } from 'react'; import IndexPage from './IndexPage.js'; import ArtistPage from './ArtistPage.js'; import Layout from './Layout.js'; export default function App() { return ( <Suspense fallback={<BigSpinner />}> <Router /> </Suspense> ); } function Router() { const [page, setPage] = useState('/'); function navigate(url) { startTransition(() => { setPage(url); }); } let content; if (page === '/') { content = ( <IndexPage navigate={navigate} /> ); } else if (page === '/the-beatles') { content = ( <ArtistPage artist={{ id: 'the-beatles', name: 'The Beatles', }} /> ); } return ( <Layout> {content} </Layout> ); } function BigSpinner() { return <h2>🌀 Loading...</h2>; }
একটি ট্রান্সিশন সবগুলো কন্টেন্ট লোড হওয়ার জন্য অপেক্ষা করে না। এটি কেবলমাত্র ইতিমধ্যে প্রকাশিত কন্টেন্ট লুকানো এড়ানোর জন্য যতক্ষণ সময় প্রয়োজন ততক্ষণ অপেক্ষা করে। উদাহরণস্বরূপ, ওয়েবসাইটের Layout ইতিমধ্যেই প্রকাশিত হয়ে গেছে, তাই এটিকে লোডিং স্পিনারের পিছনে লুকানো ভাল হবে না। তবে, Albums কে ঘিরে নেস্টেড Suspense boundary টি নতুন, তাই ট্রান্সিশনটি এর জন্য অপেক্ষা করে না।
ট্রান্সিশন ঘটছে এটি ইন্ডিকেট করা
উপরের উদাহরণটিতে, আপনি যদি একবার বাটনটিতে ক্লিক করেন দেখবেন কোনো দৃশ্যমান ইংগিত নেই যে একটি নেভিগেশনের অগ্রগতি হচ্ছে। একটি ইন্ডিকেটর যোগ করতে আপনি startTransition এর স্থানে useTransition ব্যবহার করতে পারেন যা আপনাকে একটি boolean মান isPending দেয়। নিচের উদাহরণে ওয়েবসাইটের হেডার স্টাইল পরিবর্তন করার সময় একটি ট্রান্সিশন ঘটছে তা দেখানোর জন্য এটি ব্যবহার করা হয়েছেঃ
import { Suspense, useState, useTransition } from 'react'; import IndexPage from './IndexPage.js'; import ArtistPage from './ArtistPage.js'; import Layout from './Layout.js'; export default function App() { return ( <Suspense fallback={<BigSpinner />}> <Router /> </Suspense> ); } function Router() { const [page, setPage] = useState('/'); const [isPending, startTransition] = useTransition(); function navigate(url) { startTransition(() => { setPage(url); }); } let content; if (page === '/') { content = ( <IndexPage navigate={navigate} /> ); } else if (page === '/the-beatles') { content = ( <ArtistPage artist={{ id: 'the-beatles', name: 'The Beatles', }} /> ); } return ( <Layout isPending={isPending}> {content} </Layout> ); } function BigSpinner() { return <h2>🌀 Loading...</h2>; }
নেভিগেশনে Suspense boundary গুলো রিসেট করা
Transition চলাকালে React আগে প্রকাশিত কনটেন্ট লুকানো এড়িয়ে চলে। তবে অন্য কোনো ব্যবহারকারীর প্রোফাইলের মতো ভিন্ন কনটেন্টে নেভিগেট করলে, আগের কনটেন্টের বদলে বাউন্ডারির fallback দেখানো প্রয়োজন। একটি key দিয়ে আপনি এই পার্থক্যটি বোঝাতে পারেন:
<ProfilePage key={queryParams.id} />আলাদা key থাকলে React প্রোফাইলগুলোকে ভিন্ন কনটেন্ট হিসেবে বিবেচনা করে এবং নেভিগেশনের সময় Suspense বাউন্ডারি রিসেট করে। বাউন্ডারিতে বা তার উপরের কোনো কম্পোনেন্টে key দেওয়া যায়। Suspense-এর সাথে সমন্বিত রাউটারগুলোর এটি স্বয়ংক্রিয়ভাবে করার কথা।
নিচের উদাহরণে, প্রোফাইল পেইজ খুললে প্রথম প্রোফাইলটি লোড হয়। “Bob” চাপলে অন্য প্রোফাইলে নেভিগেট করা হয় এবং key বাউন্ডারিটি রিসেট করে। তাই আগের ব্যবহারকারীর পরিচিতির বদলে fallback দেখানো হয়। key সরিয়ে দেখুন: পরের পরিচিতি লোড হওয়ার সময় আগেরটি দেখা যেতে থাকে:
import { Suspense, useState, startTransition } from 'react'; import Bio from './Bio.js'; import { fetchBio } from './data.js'; export default function ProfilePage() { const [user, setUser] = useState(() => ({ id: 'alice', bioPromise: fetchBio('alice'), })); function navigate(id) { startTransition(() => { setUser({ id, bioPromise: fetchBio(id) }); }); } return ( <> <button onClick={() => navigate('alice')}> Alice </button> <button onClick={() => navigate('bob')}> Bob </button> <Suspense key={user.id} fallback={<p>⌛ Loading profile...</p>}> <Bio bioPromise={user.bioPromise} /> </Suspense> </> ); }
Server errors এবং client-only কন্টেন্টের জন্য একটি fallback দেওয়া
আপনি যদি streaming server rendering APIs (অথবা এদের উপর নির্ভরকারী একটি ফ্রেমওয়ার্ক) এর কোনোটি ব্যবহার করেন, React আপনার <Suspense> boundary গুলোও ব্যবহার করবে সার্ভার এর এরর গুলো তত্ত্বাবধান করতে। যদি কোনো কম্পোনেন্ট সার্ভারে এরর থ্রো করে, React সার্ভার রেন্ডার বন্ধ করবে না। বরং, এর সবচেয়ে কাছের <Suspense> কম্পোনেন্টটি খুঁজে বের করবে এবং তার fallback (যেমন একটি স্পিনার), তৈরি করা সার্ভার HTML এর মধ্যে যোগ করবে। ব্যবহারকারী প্রথমে একটি স্পিনার দেখবে।
ক্লায়েন্টে, একই কম্পোনেন্টটি React আবার রেন্ডার করার চেষ্টা করবে। যদি ক্লায়েন্টেও এরর হয়, React এররটি থ্রো করবে এবং সবচেয়ে কাছের Error Boundary টি দেখাবে। তবে, যদি ক্লায়েন্টে এরর না হয়, React এরর টি ক্লায়েন্টে দেখাবে না যেহেতু কন্টেন্টটি শেষ পর্যন্ত সফলভাবে প্রদর্শিত হয়েছিল।
সার্ভারে রেন্ডার হওয়া থেকে কিছু কম্পোনেন্ট তুলে নিতে চাইলে আপনি এটি ব্যবহার করতে পারেন। এটি করতে, সার্ভার এনভায়রনমেন্ট এ এরর থ্রো করুন এবং তারপর তাদের HTML গুলো fallback দিয়ে পরিবর্তন করতে তাদের একটি <Suspense> বাউন্ডারির মধ্যে রেখে দিনঃ
<Suspense fallback={<Loading />}>
<Chat />
</Suspense>
function Chat() {
if (typeof window === 'undefined') {
throw Error('Chat should only render on the client.');
}
// ...
}সার্ভার HTML টির মধ্যেই লোডিং নির্দেশকটি থাকবে। এটি ক্লায়েন্টে Chat কম্পোনেন্ট দ্বারা প্রতিস্থাপিত হবে।
শুধু ব্রাউজারে রেন্ডার করা কনটেন্টের জন্য fallback দেখানো
একটি Suspense বাউন্ডারি শুধু ব্রাউজারে রেন্ডার করা কম্পোনেন্টের জন্য fallback দেখাতে পারে। কম্পোনেন্টটিকে <Suspense> দিয়ে ঘিরে তার ভেতরে use(browser()) কল করুন।
প্রাথমিক HTML-এ লোডিং fallback দেখতে Reload চাপুন। hydration-এর পরে React localStorage থেকে লোড করা খসড়া দেখায়।
import { Suspense, use, useState } from 'react'; import { browser } from 'react-dom'; function SavedDraft() { use(browser('The draft is stored in localStorage.')); const [draft, setDraft] = useState( () => localStorage.getItem('draft') ?? '' ); function handleChange(event) { const nextDraft = event.target.value; setDraft(nextDraft); localStorage.setItem('draft', nextDraft); } return ( <label> Draft: <textarea value={draft} onChange={handleChange} rows={4} cols={30} /> </label> ); } export default function App() { return ( <> <h1>Saved draft</h1> <Suspense fallback={<p>Loading draft...</p>}> <SavedDraft /> </Suspense> </> ); }
সার্ভার রেন্ডারিংয়ের সময় React HTML-এ Suspense বাউন্ডারির fallback যুক্ত করে। ব্রাউজারে React সেই fallback-এর বদলে সংরক্ষিত খসড়াটি দেখায়।
stylesheet লোড হওয়ার জন্য অপেক্ষা করা
<link rel="stylesheet"> এবং precedence prop দিয়ে রেন্ডার করা stylesheet লোড না হওয়া পর্যন্ত, একটি নির্দিষ্ট সময়সীমার মধ্যে, Suspense বাউন্ডারির কনটেন্ট প্রকাশ আটকে থাকে। এতে কনটেন্ট স্টাইল ছাড়া দেখা যায় না।
নিচের উদাহরণে Card কম্পোনেন্ট precedence-সহ একটি stylesheet রেন্ডার করে। “Show card” চাপুন: stylesheet লোড না হওয়া পর্যন্ত React fallback দেখায়, তারপর স্টাইল প্রয়োগ করা কার্ডটি প্রকাশ করে।
তুলনার জন্য, দ্বিতীয় বাটনটি React ছাড়াই আলাদা document-এ একই আপডেট করে। stylesheet-এর জন্য অপেক্ষা করা হয় না বলে কার্ডের টেক্সট প্রথমে fallback ফন্টে দেখা যায়, পরে ফন্ট বদলে যায়:
import { Suspense, useState, startTransition } from 'react'; import { freshStylesheetUrl } from './styles.js'; import VanillaCard from './VanillaCard.js'; function Card({ href }) { return ( <> <link rel="stylesheet" href={href} precedence="default" /> <div className="fancy-card">This card uses a font from the stylesheet.</div> </> ); } export default function App() { const [href, setHref] = useState(null); return ( <> <button onClick={() => { startTransition(() => { setHref(freshStylesheetUrl()); }); }}> Show card </button> {href && ( <Suspense fallback={<p>⌛ Loading styles...</p>}> <Card href={href} /> </Suspense> )} <hr /> <VanillaCard /> </> ); }
Suspense কনটেন্ট থেকে অ্যানিমেশন চালানো
fallback থেকে কনটেন্টে পরিবর্তনের অ্যানিমেশন চালাতে Suspense-এর সাথে <ViewTransition> ব্যবহার করা যায়। বাউন্ডারিটিকে <ViewTransition> দিয়ে ঘিরে দিলে React এই পরিবর্তনকে একটি আপডেট হিসেবে বিবেচনা করে। ডিফল্টভাবে fallback ধীরে মিলিয়ে গিয়ে কনটেন্ট ফুটে ওঠে:
import {ViewTransition, useState, startTransition, Suspense} from 'react'; import {Video, VideoPlaceholder} from './Video'; import {useLazyVideoData} from './data'; function LazyVideo() { const video = useLazyVideoData(); return <Video video={video} />; } export default function Component() { const [showItem, setShowItem] = useState(false); return ( <> <button onClick={() => { startTransition(() => { setShowItem((prev) => !prev); }); }}> {showItem ? '➖' : '➕'} </button> {showItem ? ( <ViewTransition> <Suspense fallback={<VideoPlaceholder />}> <LazyVideo /> </Suspense> </ViewTransition> ) : null} </> ); }
ফন্ট লোড হওয়ার জন্য অপেক্ষা করা
<ViewTransition> দিয়ে Suspense বাউন্ডারির কনটেন্ট প্রকাশের অ্যানিমেশন চালালে, কনটেন্টে ব্যবহৃত নতুন ফন্ট লোড হওয়ার জন্য React সর্বোচ্চ 500 মিলিসেকেন্ড অপেক্ষা করে। এতে টেক্সট হঠাৎ fallback ফন্টে দেখা যায় না। এটি শুধু <ViewTransition> আপডেটের সময় ঘটে।
নিচের উদাহরণে Suspense বাউন্ডারিটিকে <ViewTransition> দিয়ে ঘেরা হয়েছে, আর ডেটা লোড হওয়ার সময় Quote কম্পোনেন্ট suspend করে। উদ্ধৃতিটি রেন্ডার করার সময় একটি stylesheet লোড হয়, যা ফন্টটি যোগ করে। stylesheet ও ফন্ট লোড হওয়ার সময় React fallback দেখিয়ে রাখে। ফন্টটি 500 মিলিসেকেন্ডের মধ্যে লোড হলে, উদ্ধৃতিটি শুরু থেকেই সেই ফন্টে দেখা যায়।
তুলনার জন্য, দ্বিতীয় বাটনটি React ছাড়াই একই আপডেট করে। ফন্টের জন্য অপেক্ষা করা হয় না বলে টেক্সট প্রথমে fallback ফন্টে দেখা যায়, পরে ফন্ট বদলে যায়:
import { ViewTransition, Suspense, use, useState, startTransition, } from 'react'; import { fetchQuote } from './data.js'; import { freshFontUrl, freshStylesheetUrl, } from './font.js'; import VanillaQuote from './VanillaQuote.js'; function Quote({ stylesheet }) { const quote = use(fetchQuote()); return ( <> <link rel="stylesheet" href={stylesheet} precedence="default" /> <p className="quote fancy">{quote}</p> </> ); } export default function App() { const [stylesheet, setStylesheet] = useState(null); return ( <> <button onClick={() => { startTransition(() => { setStylesheet(freshStylesheetUrl()); }); }}> Show quote </button> {stylesheet && ( <ViewTransition> <Suspense fallback={<p className="quote">⌛ Loading quote...</p>}> <Quote stylesheet={stylesheet} /> </Suspense> </ViewTransition> )} <hr /> <VanillaQuote /> </> ); }
ছবি লোড হওয়ার জন্য অপেক্ষা করা
<ViewTransition>-এর ভেতরে কোনো Suspense বাউন্ডারি কনটেন্ট প্রকাশ করলে, অ্যানিমেশন শুরুর আগে React দৃশ্যমান ছবিগুলো লোড হওয়ার জন্য সর্বোচ্চ 500 মিলিসেকেন্ড অপেক্ষা করে। কোনো ছবিতে loading="lazy" বা onLoad handler দিলে সেই ছবির জন্য অপেক্ষা করা হয় না।
একই বাউন্ডারি <ViewTransition>-এর ভেতরে ও বাইরে রেখে তুলনা করুন। ভেতরে থাকলে, ছবি লোড হওয়ার সময় React সর্বোচ্চ 500 মিলিসেকেন্ড প্রোফাইলের skeleton দেখিয়ে রাখে, যাতে ছবিসহ কার্ডটি প্রকাশ করা যায়। বাইরে থাকলে, Promise resolve হওয়া মাত্রই Suspense skeleton দেখানো বন্ধ করে। ছবি তখনও লোড হতে থাকলে, প্রথমে কার্ড দেখা যায়, পরে হঠাৎ ছবিটি দেখা দেয়:
import { ViewTransition, Suspense, use, useState, } from 'react'; import { fetchImageSrc } from './image.js'; function Profile({ cacheKey }) { const src = use(fetchImageSrc(cacheKey)); return ( <div className="card"> <img src={src} alt="" width={80} height={80} /> <p>Jack Pope</p> </div> ); } function ProfilePlaceholder() { return ( <div className="card"> <div className="avatar-placeholder" /> <p className="name-placeholder"> </p> </div> ); } export default function App() { const [showWithTransition, setShowWithTransition] = useState(false); const [showWithoutTransition, setShowWithoutTransition] = useState(false); return ( <> <button onClick={() => setShowWithTransition(true)}> Show profile with View Transition </button> {showWithTransition && ( <ViewTransition> <Suspense fallback={<ProfilePlaceholder />}> <Profile cacheKey="with-transition" /> </Suspense> </ViewTransition> )} <hr /> <button onClick={() => setShowWithoutTransition(true)}> Show profile without View Transition </button> {showWithoutTransition && ( <Suspense fallback={<ProfilePlaceholder />}> <Profile cacheKey="without-transition" /> </Suspense> )} </> ); }
ফন্ট, ছবি ও stylesheet-এর লোডিং সমন্বয় করা
একটি Suspense বাউন্ডারি একইসাথে ডেটা, stylesheet, ফন্ট ও ছবির জন্য অপেক্ষা করতে পারে। ফন্ট ও ছবির জন্য অপেক্ষা করা হয় শুধু <ViewTransition> আপডেটের সময়। নিচের উদাহরণে, ডেটা লোড হওয়ার সময় ProfileCard কম্পোনেন্ট suspend করে। এটি precedence-সহ একটি stylesheet, নতুন ফন্টের টেক্সট এবং একটি প্রতিকৃতি রেন্ডার করে। ডেটা ও stylesheet লোড হওয়ার সময় React skeleton দেখিয়ে রাখে। তারপর <ViewTransition> দিয়ে কনটেন্ট প্রকাশের আগে ফন্ট ও ছবির জন্য অপেক্ষা করা হয়, যাতে কার্ডটি সম্পূর্ণ অবস্থায় দেখা যায়।
তুলনার জন্য, React ছাড়া সংস্করণটি একই ডেটা লোড করে এবং প্রতিটি resource যখন পৌঁছায় তখনই দেখায়:
import { ViewTransition, Suspense, use, useState, startTransition } from 'react'; import { fetchQuote } from './data.js'; import { freshStylesheetUrl, freshImageUrl } from './resources.js'; import VanillaProfileCard from './VanillaProfileCard.js'; function ProfileCard({ resources }) { const quote = use(resources.quotePromise); return ( <> <link rel="stylesheet" href={resources.stylesheet} precedence="default" /> <div className="profile-card"> <img src={resources.image} alt="" width={80} height={80} /> <div> <p className="name">Jack Pope</p> <p className="bio">{quote}</p> </div> </div> </> ); } function ProfileCardPlaceholder() { return ( <div className="profile-card"> <div className="avatar-placeholder" /> <div> <p className="name name-placeholder"> </p> <p className="bio bio-placeholder"> </p> </div> </div> ); } export default function App() { const [resources, setResources] = useState(null); return ( <> <button onClick={() => { startTransition(() => { setResources({ quotePromise: fetchQuote(), stylesheet: freshStylesheetUrl(), image: freshImageUrl(), }); }); }}> Show profile </button> {resources && ( <ViewTransition> <Suspense fallback={<ProfileCardPlaceholder />}> <ProfileCard resources={resources} /> </Suspense> </ViewTransition> )} <hr /> <VanillaProfileCard /> </> ); }
সমস্যার সমাধান করা
কিভাবে আমি একটি আপডেটের সময় fallback দ্বারা UI প্রতিস্থাপিত হওয়াকে রোধ করবো?
fallback দ্বারা দৃশ্যমান UI প্রতিস্থাপন করা হলে ব্যবহারকারীর জন্য এটি অস্বাভাবিক অভিজ্ঞতা তৈরি করে। এটি ঘটতে পারে যখন একটি আপডেট কোনো কম্পোনেন্টকে থামিয়ে দেয় এবং সবচেয়ে কাছের Suspense boundary ইতিমধ্যেই ব্যবহারকারীর জন্য কন্টেন্ট দেখাচ্ছে।
এটি ঘটানো থেকে বিরত থাকার জন্য আপডেটটিকে অতি জরুরি নয় হিসেবে চিহ্নিত করুন startTransition। একটি ট্রান্সিশনের সময়, React একটি অবাঞ্ছিত fallback দেখানো থেকে বিরত রাখতে পর্যাপ্ত পরিমাণ ডেটা লোড না হওয়ার পর্যন্ত অপেক্ষা করবেঃ
function handleNextPageClick() {
// If this update suspends, don't hide the already displayed content
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}এটা বিদ্যমান কন্টেন্ট লুকানো থেকে এড়িয়ে যাবে। তবে, যেকোনো নতুন রেন্ডার করা Suspense বাউন্ডারি তাৎক্ষণাৎ fallback দেখাবে যাতে UI ব্লক না হয় এবং কন্টেন্ট পাওয়া মাত্র ব্যবহারকারী তা দেখতে পারে।
React শুধুমাত্র অতি জরুরি নয় এমন আপডেট এর সময়ই অস্বাভাবিক fallback গুলো প্রতিরোধ করবে। এটি একটি রেন্ডারকে বিলম্ব করবে না যদি এটি কোনো জরুরী আপডেটের ফলাফল হয়ে থাকে। আপনাকে অবশ্যই startTransition বা useDeferredValue এর মত API ব্যবহার করতে হবে।
আপনার রাউটারটি যদি Suspense এর সাথে সংযোজিত থাকে, তাহলে এটি তার আপডেটগুলোকে স্বয়ংক্রিয়ভাবেই startTransition এর মধ্যে রেখে দিবে।