Concurrent UI Patterns (Experimental)
Perhatian:
Laman ini menjelaskan fitur eksperimental yang belum tersedia dalam versi rilis yang stabil. Jangan mengandalkan build eksperimental dalam aplikasi React versi produksi. Fitur ini dapat berubah secara signifikan dan tanpa peringatan sebelum menjadi bagian dari React.
Dokumentasi ini ditujukan untuk pengguna awal dan orang-orang yang penasaran. Kalau Anda baru menggunakan React, jangan khawatir tentang fitur ini — anda tidak perlu mempelajarinya sekarang.
Biasanya, ketika kita mengubah state, kita akan langsung melihat perubahannya di layar. Ini masuk akal karena kita ingin aplikasi responsif terhadap input user. Tetapi, mungkin ada kasus dimana kita lebih memilih untuk menunda perubahan yang terjadi di layar
Contohnya, ketika kita pindah laman, dan belum ada data yang tersedia untuk laman selanjutnya, mungkin kita akan merasa frustasi ketika melihat laman dengan loading. Kita mungkin lebih memilih untuk tetap berada di laman sebelumnya. Meng-implementasi pola ini secara historis sulit dalam React. Concurrent Mode menawarkan seperangkat alat baru untuk melakukan itu.
Transisi
Mari buka kembali demo berikut dari the laman sebelumnya mengenai Suspense for Data Fetching.
Ketika kita klik Tombol “Next” untuk mengubah profil aktif, data laman sebelumnya langsung menghilang, dan kita melihat indikator loading untuk seluruh laman lagi. Kita bisa menyebut hal ini loading state yang “tidak diinginkan”. Akan lebih baik jika kita bisa “melewatinya” dan menunggu konten tersebut selesai di load sebelum pindah ke laman baru
React punya Hook khusus useTransition()
untuk menangani masalah ini.
Kita bisa menggunakannya dengan 3 langkah.
Pertama, Kita harus menggunakan Concurrent Mode. Kita akan membahas lebih lanjut tentang mengadopsi Concurrent Mode lain waktu, untuk sekarang kita hanya perlu tahu kalau kita akan menggunakan ReactDOM.createRoot()
daripada ReactDOM.render()
agar fitur berikut dapat bekerja:
const rootElement = document.getElementById("root");
// Opt into Concurrent Mode
ReactDOM.createRoot(rootElement).render(<App />);
Selanjutnya kita import hookuseTransition
dari React:
import React, { useState, useTransition, Suspense } from "react";
Terakhir, kita pakai hook tersebut di dalam komponen App
:
function App() {
const [resource, setResource] = useState(initialResource);
const [startTransition, isPending] = useTransition({ timeoutMs: 3000 }); // ...
Dengan sendirinya, kode ini tidak melakukan apapun. Kita akan menggunakan nilai return dari Hook ini untuk mempersiapkan transisi state kita. Ada dua nilai yang di return dari useTransition
“:
startTransition
adalah fungsi. Fungsi ini akan kita gunakan untuk memberi tahu React state mana yang ingin kita tunda.isPending
adalah boolean. React memberi tahu apakah transisi sedang terjadi atau tidak.
Kita akan menggunakannya seperti berikut.
Kita menggunakan objek konfigurasi ke useTransition
. Properti timeoutMs
menspesifikasi berapa lama kita ingin menunggu transisi untuk selesai. Dengan mengoper {timeoutMs: 3000}
, berarti kita mengatakan “Jika profil perlu lebih dari 3 detik untuk load, tampilkan loading berputar — tapi sebelum timeout kita bisa tetap memperlihatkan layar sebelumnya”.
Setstate di Dalam Transisi
Handler klik tombol “Next” kita, mengubah state yang mengganti profile saat ini:
<button
onClick={() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId)); }}
>
Kita akan membungkus perubahan state tersebut didalam startTransition
. Dengan begitu, kita memberitahu React untuk Boleh delay update state tersebut jika perubahan tersebut membuat loading state yang tidak diinginkan:
<button
onClick={() => {
startTransition(() => { const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
}); }}
>
Tekan “Next” beberapa kali. Jika diperhatikan, akan terasa bedanya. Daripada langsung melihat layar kosong ketika klik, sekarang kita melihat layar sebelumnya untuk beberapa saat. Ketika data sudah dimuat, React mengarahkan kita ke layar baru.
Jika kita membuat response API menunggu selama 5 detik, kita bisa memastikan kalau React “menyerah” dan langsung mengarahkan kita ke layar selanjutnya setelah 3 detik. Ini karena kita telah melewati batas {timeoutMs: 3000}
untuk useTransition()
. Contohnya, jika kita set {timeoutMs: 60000}
, React akan menunggu selama semenit penuh.
Menambah Indikator Tunggu
Ada sesuatu yang masih merasa tidak benar di contoh kita sebelumnya. Memang terasa lebih bagus ketika kita tidak melihat state memuat yang “buruk”. Namun lebih buruk lagi ketika tidak ada indikasi progres! Ketika kita mengklik “Next”, tidak ada yang terjadi dan aplikasi kita terasa seperti rusak.
Pemanggilan useTransition()
mengembalikan dua nilai: startTransition
dan isPending
.
const [startTransition, isPending] = useTransition({ timeoutMs: 3000 });
Kita telah menggunakan startTransition
untuk membungkus perubahan state. Sekarang kita juga akan menggunakan isPending
. React memberikan nilai boolean ini kepada kita supaya kita dapat mengetahui apakah kita sedang menunggu transisi ini selesai. Kita menggunakan ini untuk mengindikasikan sesuatu sedang terjadi:
return (
<>
<button
disabled={isPending} onClick={() => {
startTransition(() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
});
}}
>
Next
</button>
{isPending ? " Loading..." : null} <ProfilePage resource={resource} />
</>
);
Sekarang, ini terasa lebih baik! Ketika kita mengklik Next, tombolnya di-disable karena mengkliknya beberapa kali tidak masuk akal. Dan teks “Loading…” memberitahu pengguna bahwa aplikasi tidak sedang freeze.
Me-review Perubahan
Mari kita ulas kembali perubahan-perubahan yang kita buat sejak contoh awal:
function App() {
const [resource, setResource] = useState(initialResource);
const [startTransition, isPending] = useTransition({ timeoutMs: 3000 }); return (
<>
<button
disabled={isPending} onClick={() => {
startTransition(() => { const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
}); }}
>
Next
</button>
{isPending ? " Loading..." : null} <ProfilePage resource={resource} />
</>
);
}
Menambahkan transisi ini hanya memerlukan 7 baris kode:
- Kita meng-import Hook
useTransition
dan menggunakannya dalam komponen yang merubah state. - Kita mengoper
{timeoutMs: 3000}
untuk bertahan di layar sebelumnya paling lama 3 detik. - Kita membungkus perubahan state dalam
startTransition
untuk memberitahu React tidak apa-apa menundanya. - Kita menggunakan
isPending
untuk mengkomunikasikan progres transisi state ke pengguna dan men-disable tombolnya.
Hasilnya, mengklik “Next” tidak melakukan transisi state secara langsung ke state memuat yang “tidak diinginkan”, namun bertahan di layar berikutnya dan mengkomunikasikan progres lewatnya.
Di Mana Perubahan Terjadi?
Mengimplementasi ini tidak terlalu sulit. Namun, jika Anda mulai memikirkan bagaimana cara kerjanya, akan menjadi lebih memeras otak. Jika kita mengeset state-nya, bagaimana caranya kita tidak melihat hasilnya secara langsung? Di mana <ProfilePage>
yang selanjutnya me-render?
Jelasnya, kedua “versi” <ProfilePage>
ada dalam waktu yang sama. Kita tahu versi yang lama ada karena kita dapat melihatnya di layar dan bahkan menunjukkan indikator progress. Dan kita juga tahu versi yang baru berada di suatu tempat, karena ini yang kita inginkan!
Tapi bagaimana kedua versi komponen yang sama dapat tersedia dalam waktu yang sama?
Di sinilah kita mulai menuju ke akarnya Concurrent Mode. Kita telah mengungkapkan sebelumnya Concurrent Mode seperti React mengerjakan perubahan state dalam sebuah “branch”. Cara lain kita dapat memahami konsepnya adalah membungkus perubahan state di dalam startTransition
memulai proses render “di alam semesta yang berbeda”, layaknya film fiksi ilmiah. Kita tidak dapat “melihat” alam semesta tersebut secara langsung — namun kita dapat mendapatkan sinyal darinya yang memberitahukan sesuatu sedang terjadi (isPending
). Ketika perubahan telah siap, “alam semesta” kita menyatu kembali, dan kita melihat hasilnya di layar!
Bermainlah lagi dengan demo-nya, bayangkan hal itu terjadi.
Tentu saja, dua versi dari sebuah diagram komponen me-render di waktu yang sama hanyalah ilusi, layaknya semua program berjalan di komputer Anda di waktu yang sama hanyalah ilusi. Sebuah sistem operasi beralih antar aplikasi dalam waktu yang sangat cepat. Seperti itu jugalah React dapat beralih antar versi diagram komponen dalam layar dan versi yang sedang “dipersiapkan” untuk ditampilkan selanjutnya.
API seperti useTransition
memungkinkan Anda untuk berfokus kepada user experience yang diinginkan, dan tidak terlalu memikirkan teknis implementasinya. Namun membayangkan perubahan yang dibungkus dalam startTransition
terjadi “di dalam branch” atau “di dunia lain” tetap menjadi metafor yang membantu.
Transisi di Setiap Tempat
Seperti yang telah kita pelajari di walkthrough Suspense, setiap komponen dapat “menunda” setiap waktu ketika data yang diperlukan belum tersedia. Kita dapat menempatkan batas-batas <Suspense>
secara strategis di tempat-tempat berbeda dalam diagram komponen untuk menangani ini, namun cara ini tidak selalu cukup.
Mari kita kembali ke demo pertama Suspense ketika hanya ada satu profil pengguna. Saat ini, komponen ini hanya menarik data sekali. Kita ingin menambahkan tombol “Refresh” untuk mengecek perubahan dari peladen.
Percobaan pertama kita mungkin terlihat seperti ini:
const initialResource = fetchUserAndPosts();
function ProfilePage() {
const [resource, setResource] = useState(initialResource);
function handleRefreshClick() { setResource(fetchUserAndPosts()); }
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<button onClick={handleRefreshClick}> Refresh </button> <Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>
);
}
Dalam contoh ini, kita memulai penarikan data di saat memuat dan setiap kali Anda menekan “Refresh”. Kita menaruh hasil panggilan fetchUserAndPosts()
ke dalam state sehingga komponen-komponen di bawahnya dapat memulai membaca data baru dari request yang kita baru jalankan.
Kita dapat melihat di contoh ini bahwa menekan tombol “Refresh” berhasil. Komponen <ProfileDetails>
dan <ProfileTimeline>
menerima props resource
baru yang merepresentasikan data baru, komponen-komponen ini “menunda” perunahan karena kita belum mendapatkan response, dan kita dapat melihat kondisi fallback. Ketika response telah dimuat, kita dapat melihat daftar postingan yang diperbarui (API tiruan kita menambahkannya setiap 3 detik).
Namun, pengalaman pengguna akan menjadi tidak enak. Ketika kita sedang browsing sebuah halaman, namun tiba-tiba seluruh halaman diubah menjadi state loading sesaat sebelum kita akan berinteraksi dengannya, akan terasa membingungkan. Seperti sebelumnya, untuk menghindari menampilkan state loading yang tidak diinginkan, kita dapat membungkus perubahan state di dalam transisi:
function ProfilePage() {
const [startTransition, isPending] = useTransition({ // Wait 10 seconds before fallback timeoutMs: 10000 }); const [resource, setResource] = useState(initialResource);
function handleRefreshClick() {
startTransition(() => { setResource(fetchProfileData()); }); }
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<button
onClick={handleRefreshClick}
disabled={isPending}
>
{isPending ? "Refreshing..." : "Refresh"} </button>
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>
);
}
Ini terasa lebih baik! Menekan tombol “Refresh” tidak akan menarik kita keluar dari halaman yang sedang kita liihat. Kita melihat sesuatu sedang dimuat secara “inline”, dan ketika data telah siap, baru ditampilkan.
Memadukan Transisi ke Dalam Design System
Kita dapat melihat bahwa penggunaan useTransition
sudah sangat diperlukan. Setiap tekanan tombol atau interaksi yang memerlukan penundaan komponen perlu dibungkus di dalam useTransition
untuk menghindari menyembunyikan sesuatu yang pengguna sedang berinteraksi secara tidak sengaja.
Ini dapat mengasilkan penggunaan kode yang repetitif di berbagai komponen. Inilah kenapa secara umum kita perlu memadukan useTransition
ke dalam komponen design system di dalam aplikasi kita. Sebagai contoh, kita dapat mengekstrak logika transisi ke dalam komponen <Button>
kita:
function Button({ children, onClick }) {
const [startTransition, isPending] = useTransition({
timeoutMs: 10000
});
function handleClick() {
startTransition(() => { onClick(); }); }
const spinner = (
// ...
);
return (
<>
<button
onClick={handleClick}
disabled={isPending} >
{children}
</button>
{isPending ? spinner : null} </>
);
}
Perlu dicatat bahwa tombol ini tidak memperdulikan state apa yang kita ubah. Ia membungkus perubahan state apapun yang terjadi di dalam handler onClick
ke dalam sebuah transisi. Karena <Button>
sekarang yang melakukan pembuatan transisi, komponen <ProfilePage>
tidak perlu membuatnya sendiri:
function ProfilePage() {
const [resource, setResource] = useState(initialResource);
function handleRefreshClick() { setResource(fetchProfileData()); }
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<Button onClick={handleRefreshClick}> Refresh </Button> <Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>
);
}
Ketika tombol diklik, ia akan memulai transisi dan memanggil props.onClick()
di dalamnya — yang memanggil handleRefreshClick
di dalam komponen <ProfilePage>
. Kita memulai menarik data yang baru, tapi tidak memanggil fallback karena kita di dalam sebuah transisi, dan timeout dengan durasi 10 detik yang dispesifikasikan di dalam panggilan useTransition
belum terlewat. Ketika transisi sedang menunggu, tombol akan menunjukkan indikator loading secara inline.
Kita dapat melihat bagaimana mode Concurrent membantu kita mendapatkan pengalaman pengguna yang baik tanpa mengorbankan isolasi dan modularitas dari komponen. React mengkoordinasikan transisinya.
Tiga Langkah
Sampai titik ini, kita telah mendiskusikan berbagai macam kondisi visual berbeda yang ditelusuri sebuah pembaruan data. Di bagian ini, kita akan memberikan mereka nama dan menjelaskan perjalanan di antaranya.

Di paling akhir, terdapat state Complete. Inilah state yang ingin kita tuju. State ini merepresentasikan moment di mana layar selanjutnya di-render secara penuh dan tidak lagi memuat data.
Namun sebelum layar kita menjadi Complete, kita mungkin perlu memuat data atau kode. Ketika kita berada di layar selanjutnya, namun beberapa bagian data tersebut masih memuat, kita sebut state tersebut sebagai Skeleton.
Akhirnya, ada dua cara utama yang membawa kita ke state Skeleton. Kita akan mengilustrasikan perbedaan di antaranya dengan contoh konkret.
Default: Receded → Skeleton → Complete
Bukalah contoh berikut dan tekan “Open Profile”. Anda akan melihat beberapa state visual satu persatu:
- Receded: Untuk beberapa saat, Anda akan melihat kondisi fallback
<h1>Loading the app...</h1>
. - Skeleton: Anda akan melihat komponen
<ProfilePage>
dengan<h2>Loading posts...</h2>
di dalamnya. - Complete: Anda akan melihat komponen
<ProfilePage>
tanpa fallback. Seluruh data dimuat.
Bagaimana caranya kita memisahkan state Receded dan Skeleton? Perbedaan di antara keduanya adalah state Receded terasa seperti “mengambil langkah mundur” kepada pengguna, dan state Skeleton terasa seperti “mengambil langkah maju” dalam perjalanan kita menampilkan lebih banyak konten.
Dalam contoh berikut, perjalanan kita dimulai di <HomePage>
:
<Suspense fallback={...}>
{/* previous screen */}
<HomePage />
</Suspense>
Setelah kita mengeklik, React memulai me-render layar selanjutnya:
<Suspense fallback={...}>
{/* next screen */}
<ProfilePage>
<ProfileDetails />
<Suspense fallback={...}>
<ProfileTimeline />
</Suspense>
</ProfilePage>
</Suspense>
<ProfileDetails>
dan <ProfileTimeline>
memerlukan data untuk di-render, jadi kedua komponen tersebut ditunda:
<Suspense fallback={...}>
{/* next screen */}
<ProfilePage>
<ProfileDetails /> {/* suspends! */} <Suspense fallback={<h2>Loading posts...</h2>}>
<ProfileTimeline /> {/* suspends! */} </Suspense>
</ProfilePage>
</Suspense>
Saat komponen ditunda, React perlu menunjukkan komponen fallback terdekat. Namun, komponen fallback terdekat dari <ProfileDetails>
terletak di tingkat atas:
<Suspense fallback={
// Kita melihat _fallback_ ini karena <ProfileDetails> <h1>Loading the app...</h1>}>
{/* next screen */}
<ProfilePage>
<ProfileDetails /> {/* suspends! */} <Suspense fallback={...}>
<ProfileTimeline />
</Suspense>
</ProfilePage>
</Suspense>
Inilah mengapa kita kita mengklik tombol, terasa seperti kita “mengambil langkah mundur”. Perbatasan <Suspense>
yang sebelumnya menunjukkan konten (<HomePage />
) yang dapat digunakan harus “mundur” untuk menunjukkan komponen fallback (<h1>Loading the app...</h1>
). Kita menyebut state tersebut Receded.
Saat kita memuat lebih banyak data, React akan mencoba ulang untuk me-render, dan <ProfileDetails>
dapat di-render secara sukses. Akhirnya, kita berada di state Skeleton. Kita melihat halaman baru dengan beberapa bagian yang hilang:
<Suspense fallback={...}>
{/* next screen */}
<ProfilePage>
<ProfileDetails />
<Suspense fallback={
// We see this fallback now because of <ProfileTimeline> <h2>Loading posts...</h2> }>
<ProfileTimeline /> {/* suspends! */} </Suspense>
</ProfilePage>
</Suspense>
Suatu saat, bagian-bagian tersebut akan dimuat juga, dan kita mencapai state Complete.
Skenario ini (Receded → Skeleton → Complete) adalah skenario bawaan. Namun, state Receded bukanlah pengalaman pengguna yang baik karena ia “menyembunyikan” informasi yang telah ada. Inilah mengapa React memungkinkan kita untuk memilih urutan berbeda (Pending → Skeleton → Complete) dengan useTransition
.
Diinginkan: Pending → Skeleton → Complete
When we useTransition
, React will let us “stay” on the previous screen — and show a progress indicator there. We call that a Pending state. It feels much better than the Receded state because none of our existing content disappears, and the page stays interactive.
You can compare these two examples to feel the difference:
- Default: Receded → Skeleton → Complete
- Preferred: Pending → Skeleton → Complete
The only difference between these two examples is that the first uses regular <button>
s, but the second one uses our custom <Button>
component with useTransition
.
Wrap Lazy Features in <Suspense>
Open this example. When you press a button, you’ll see the Pending state for a second before moving on. This transition feels nice and fluid.
We will now add a brand new feature to the profile page — a list of fun facts about a person:
function ProfilePage({ resource }) {
return (
<>
<ProfileDetails resource={resource} />
<Suspense fallback={<h2>Loading posts...</h2>}>
<ProfileTimeline resource={resource} />
</Suspense>
<ProfileTrivia resource={resource} /> </>
);
}
function ProfileTrivia({ resource }) { const trivia = resource.trivia.read(); return ( <> <h2>Fun Facts</h2> <ul> {trivia.map(fact => ( <li key={fact.id}>{fact.text}</li> ))} </ul> </> );}
If you press “Open Profile” now, you can tell something is wrong. It takes a whole seven seconds to make the transition now! This is because our trivia API is too slow. Let’s say we can’t make the API faster. How can we improve the user experience with this constraint?
If we don’t want to stay in the Pending state for too long, our first instinct might be to set timeoutMs
in useTransition
to something smaller, like 3000
. You can try this here. This lets us escape the prolonged Pending state, but we still don’t have anything useful to show!
There is a simpler way to solve this. Instead of making the transition shorter, we can “disconnect” the slow component from the transition by wrapping it into <Suspense>
:
function ProfilePage({ resource }) {
return (
<>
<ProfileDetails resource={resource} />
<Suspense fallback={<h2>Loading posts...</h2>}>
<ProfileTimeline resource={resource} />
</Suspense>
<Suspense fallback={<h2>Loading fun facts...</h2>}> <ProfileTrivia resource={resource} />
</Suspense> </>
);
}
This reveals an important insight. React always prefers to go to the Skeleton state as soon as possible. Even if we use transitions with long timeouts everywhere, React will not stay in the Pending state for longer than necessary to avoid the Receded state.
If some feature isn’t a vital part of the next screen, wrap it in <Suspense>
and let it load lazily. This ensures we can show the rest of the content as soon as possible. Conversely, if a screen is not worth showing without some component, such as <ProfileDetails>
in our example, do not wrap it in <Suspense>
. Then the transitions will “wait” for it to be ready.
Suspense Reveal “Train”
When we’re already on the next screen, sometimes the data needed to “unlock” different <Suspense>
boundaries arrives in quick succession. For example, two different responses might arrive after 1000ms and 1050ms, respectively. If you’ve already waited for a second, waiting another 50ms is not going to be perceptible. This is why React reveals <Suspense>
boundaries on a schedule, like a “train” that arrives periodically. This trades a small delay for reducing the layout thrashing and the number of visual changes presented to the user.
You can see a demo of this here. The “posts” and “fun facts” responses come within 100ms of each other. But React coalesces them and “reveals” their Suspense boundaries together.
Delaying a Pending Indicator
Our Button
component will immediately show the Pending state indicator on click:
function Button({ children, onClick }) {
const [startTransition, isPending] = useTransition({ timeoutMs: 10000
});
// ...
return (
<>
<button onClick={handleClick} disabled={isPending}>
{children}
</button>
{isPending ? spinner : null} </>
);
}
This signals to the user that some work is happening. However, if the transition is relatively short (less than 500ms), it might be too distracting and make the transition itself feel slower.
One possible solution to this is to delay the spinner itself from displaying:
.DelayedSpinner {
animation: 0s linear 0.5s forwards makeVisible;
visibility: hidden;
}
@keyframes makeVisible {
to {
visibility: visible;
}
}
const spinner = (
<span className="DelayedSpinner"> {/* ... */} </span>);
return (
<>
<button onClick={handleClick}>{children}</button>
{isPending ? spinner : null} </>
);
With this change, even though we’re in the Pending state, we don’t display any indication to the user until 500ms has passed. This may not seem like much of an improvement when the API responses are slow. But compare how it feels before and after when the API call is fast. Even though the rest of the code hasn’t changed, suppressing a “too fast” loading state improves the perceived performance by not calling attention to the delay.
Recap
The most important things we learned so far are:
- By default, our loading sequence is Receded → Skeleton → Complete.
- The Receded state doesn’t feel very nice because it hides existing content.
- With
useTransition
, we can opt into showing a Pending state first instead. This will keep us on the previous screen while the next screen is being prepared. - If we don’t want some component to delay the transition, we can wrap it in its own
<Suspense>
boundary. - Instead of doing
useTransition
in every other component, we can build it into our design system.
Cara Lain
Transitions are probably the most common Concurrent Mode pattern you’ll encounter, but there are a few more patterns you might find useful.
Memisahkan State dengan Prioritas Tinggi dan Rendah
When you design React components, it is usually best to find the “minimal representation” of state. For example, instead of keeping firstName
, lastName
, and fullName
in state, it’s usually better keep only firstName
and lastName
, and then calculate fullName
during rendering. This lets us avoid mistakes where we update one state but forget the other state.
However, in Concurrent Mode there are cases where you might want to “duplicate” some data in different state variables. Consider this tiny translation app:
const initialQuery = "Hello, world";
const initialResource = fetchTranslation(initialQuery);
function App() {
const [query, setQuery] = useState(initialQuery);
const [resource, setResource] = useState(initialResource);
function handleChange(e) {
const value = e.target.value;
setQuery(value);
setResource(fetchTranslation(value));
}
return (
<>
<input
value={query}
onChange={handleChange}
/>
<Suspense fallback={<p>Loading...</p>}>
<Translation resource={resource} />
</Suspense>
</>
);
}
function Translation({ resource }) {
return (
<p>
<b>{resource.read()}</b>
</p>
);
}
Notice how when you type into the input, the <Translation>
component suspends, and we see the <p>Loading...</p>
fallback until we get fresh results. This is not ideal. It would be better if we could see the previous translation for a bit while we’re fetching the next one.
In fact, if we open the console, we’ll see a warning:
Warning: App triggered a user-blocking update that suspended.
The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes.
Refer to the documentation for useTransition to learn how to implement this pattern.
As we mentioned earlier, if some state update causes a component to suspend, that state update should be wrapped in a transition. Let’s add useTransition
to our component:
function App() {
const [query, setQuery] = useState(initialQuery);
const [resource, setResource] = useState(initialResource);
const [startTransition, isPending] = useTransition({ timeoutMs: 5000 });
function handleChange(e) {
const value = e.target.value;
startTransition(() => { setQuery(value);
setResource(fetchTranslation(value));
}); }
// ...
}
Try typing into the input now. Something’s wrong! The input is updating very slowly.
We’ve fixed the first problem (suspending outside of a transition). But now because of the transition, our state doesn’t update immediately, and it can’t “drive” a controlled input!
The answer to this problem is to split the state in two parts: a “high priority” part that updates immediately, and a “low priority” part that may wait for a transition.
In our example, we already have two state variables. The input text is in query
, and we read the translation from resource
. We want changes to the query
state to happen immediately, but changes to the resource
(i.e. fetching a new translation) should trigger a transition.
So the correct fix is to put setQuery
(which doesn’t suspend) outside the transition, but setResource
(which will suspend) inside of it.
function handleChange(e) {
const value = e.target.value;
// Outside the transition (urgent) setQuery(value);
startTransition(() => {
// Inside the transition (may be delayed)
setResource(fetchTranslation(value));
});
}
With this change, it works as expected. We can type into the input immediately, and the translation later “catches up” to what we have typed.
Menangguhkan Sebuah Nilai
By default, React always renders a consistent UI. Consider code like this:
<>
<ProfileDetails user={user} />
<ProfileTimeline user={user} />
</>
React guarantees that whenever we look at these components on the screen, they will reflect data from the same user
. If a different user
is passed down because of a state update, you would see them changing together. You can’t ever record a screen and find a frame where they would show values from different user
s. (If you ever run into a case like this, file a bug!)
This makes sense in the vast majority of situations. Inconsistent UI is confusing and can mislead users. (For example, it would be terrible if a messenger’s Send button and the conversation picker pane “disagreed” about which thread is currently selected.)
However, sometimes it might be helpful to intentionally introduce an inconsistency. We could do it manually by “splitting” the state like above, but React also offers a built-in Hook for this:
import { useDeferredValue } from 'react';
const deferredValue = useDeferredValue(value, {
timeoutMs: 5000
});
To demonstrate this feature, we’ll use the profile switcher example. Click the “Next” button and notice how it takes 1 second to do a transition.
Let’s say that fetching the user details is very fast and only takes 300 milliseconds. Currently, we’re waiting a whole second because we need both user details and posts to display a consistent profile page. But what if we want to show the details faster?
If we’re willing to sacrifice consistency, we could pass potentially stale data to the components that delay our transition. That’s what useDeferredValue()
lets us do:
function ProfilePage({ resource }) {
const deferredResource = useDeferredValue(resource, { timeoutMs: 1000 }); return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline
resource={deferredResource} isStale={deferredResource !== resource} />
</Suspense>
</Suspense>
);
}
function ProfileTimeline({ isStale, resource }) {
const posts = resource.posts.read();
return (
<ul style={{ opacity: isStale ? 0.7 : 1 }}> {posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
The tradeoff we’re making here is that <ProfileTimeline>
will be inconsistent with other components and potentially show an older item. Click “Next” a few times, and you’ll notice it. But thanks to that, we were able to cut down the transition time from 1000ms to 300ms.
Whether or not it’s an appropriate tradeoff depends on the situation. But it’s a handy tool, especially when the content doesn’t change noticeably between items, and the user might not even realize they were looking at a stale version for a second.
It’s worth noting that useDeferredValue
is not only useful for data fetching. It also helps when an expensive component tree causes an interaction (e.g. typing in an input) to be sluggish. Just like we can “defer” a value that takes too long to fetch (and show its old value despite other components updating), we can do this with trees that take too long to render.
For example, consider a filterable list like this:
function App() {
const [text, setText] = useState("hello");
function handleChange(e) {
setText(e.target.value);
}
return (
<div className="App">
<label>
Type into the input:{" "}
<input value={text} onChange={handleChange} />
</label>
...
<MySlowList text={text} />
</div>
);
}
In this example, every item in <MySlowList>
has an artificial slowdown — each of them blocks the thread for a few milliseconds. We’d never do this in a real app, but this helps us simulate what can happen in a deep component tree with no single obvious place to optimize.
We can see how typing in the input causes stutter. Now let’s add useDeferredValue
:
function App() {
const [text, setText] = useState("hello");
const deferredText = useDeferredValue(text, { timeoutMs: 5000 });
function handleChange(e) {
setText(e.target.value);
}
return (
<div className="App">
<label>
Type into the input:{" "}
<input value={text} onChange={handleChange} />
</label>
...
<MySlowList text={deferredText} /> </div>
);
}
Now typing has a lot less stutter — although we pay for this by showing the results with a lag.
How is this different from debouncing? Our example has a fixed artificial delay (3ms for every one of 80 items), so there is always a delay, no matter how fast our computer is. However, the useDeferredValue
value only “lags behind” if the rendering takes a while. There is no minimal lag imposed by React. With a more realistic workload, you can expect the lag to adjust to the user’s device. On fast machines, the lag would be smaller or non-existent, and on slow machines, it would be more noticeable. In both cases, the app would remain responsive. That’s the advantage of this mechanism over debouncing or throttling, which always impose a minimal delay and can’t avoid blocking the thread while rendering.
Even though there is an improvement in responsiveness, this example isn’t as compelling yet because Concurrent Mode is missing some crucial optimizations for this use case. Still, it is interesting to see that features like useDeferredValue
(or useTransition
) are useful regardless of whether we’re waiting for network or for computational work to finish.
SuspenseList
<SuspenseList>
is the last pattern that’s related to orchestrating loading states.
Consider this example:
function ProfilePage({ resource }) {
return (
<>
<ProfileDetails resource={resource} />
<Suspense fallback={<h2>Loading posts...</h2>}> <ProfileTimeline resource={resource} /> </Suspense> <Suspense fallback={<h2>Loading fun facts...</h2>}> <ProfileTrivia resource={resource} /> </Suspense> </>
);
}
The API call duration in this example is randomized. If you keep refreshing it, you will notice that sometimes the posts arrive first, and sometimes the “fun facts” arrive first.
This presents a problem. If the response for fun facts arrives first, we’ll see the fun facts below the <h2>Loading posts...</h2>
fallback for posts. We might start reading them, but then the posts response will come back, and shift all the facts down. This is jarring.
One way we could fix it is by putting them both in a single boundary:
<Suspense fallback={<h2>Loading posts and fun facts...</h2>}>
<ProfileTimeline resource={resource} />
<ProfileTrivia resource={resource} />
</Suspense>
The problem with this is that now we always wait for both of them to be fetched. However, if it’s the posts that came back first, there’s no reason to delay showing them. When fun facts load later, they won’t shift the layout because they’re already below the posts.
Other approaches to this, such as composing Promises in a special way, are increasingly difficult to pull off when the loading states are located in different components down the tree.
To solve this, we will import SuspenseList
:
import { SuspenseList } from 'react';
<SuspenseList>
coordinates the “reveal order” of the closest <Suspense>
nodes below it:
function ProfilePage({ resource }) {
return (
<SuspenseList revealOrder="forwards"> <ProfileDetails resource={resource} />
<Suspense fallback={<h2>Loading posts...</h2>}>
<ProfileTimeline resource={resource} />
</Suspense>
<Suspense fallback={<h2>Loading fun facts...</h2>}>
<ProfileTrivia resource={resource} />
</Suspense>
</SuspenseList> );
}
The revealOrder="forwards"
option means that the closest <Suspense>
nodes inside this list will only “reveal” their content in the order they appear in the tree — even if the data for them arrives in a different order. <SuspenseList>
has other interesting modes: try changing "forwards"
to "backwards"
or "together"
and see what happens.
You can control how many loading states are visible at once with the tail
prop. If we specify tail="collapsed"
, we’ll see at most one fallback at a time. You can play with it here.
Keep in mind that <SuspenseList>
is composable, like anything in React. For example, you can create a grid by putting several <SuspenseList>
rows inside a <SuspenseList>
table.
Selanjutnya
Concurrent Mode offers a powerful UI programming model and a set of new composable primitives to help you orchestrate delightful user experiences.
It’s a result of several years of research and development, but it’s not finished. In the section on adopting Concurrent Mode, we’ll describe how you can try it and what you can expect.