useTransition
useTransition adalah sebuah React Hook yang memungkinkan Anda merender sebagian UI di latat belakang.
const [isPending, startTransition] = useTransition()- Referensi
- Kegunaan
- Pemecahan Masalah
- Merubah input dalam transisi tidak bekerja
- React tidak memperlakukan perubahan state saya sebagai transisi
- React tidak memperlakukan pembaruan status saya setelah
awaitsebagai Transisi - Saya ingin memanggil
useTransitiondari luar komponen - Fungsi yang saya berikan ke
startTransitiontereksekusi langsung - My state updates in Transitions are out of order
Referensi
useTransition()
Panggil useTransition pada level teratas komponen Anda untuk menandai beberapa perubahan state sebagai transisi.
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}Lihat contoh lainnya dibawah ini.
Parameters
useTransition tidak menerima parameter apa pun.
Returns
useTransition mengembalikan senarai dengan tepat dua item:
- Penanda
isPendingyang memberitahukan Anda bahwa terdapat transisi yang tertunda. - Fungsi
startTransitionyang memungkinkan Anda menandai perubahan state sebagai transisi.
startTransition(action)
Fungsi startTransition yang dikembalikan oleh useTransition memungkinkan Anda menandai perubahan state sebagai Transisi.
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}Parameters
action: Fungsi yang memperbarui suatu state dengan memanggil satu atau beberapa fungsiset. React memanggilactionsegera tanpa parameter dan menandai semua pembaruan state yang dijadwalkan secara sinkron selama panggilan fungsiactionsebagai Transisi. Semua panggilan asinkron yang ditunggu dalamactionakan disertakan dalam Transisi, tetapi saat ini memerlukan pembungkusan semua fungsisetsetelahawaitdalamstartTransitiontambahan (lihat Pemecahan Masalah). Pembaruan state yang ditandai sebagai Transisi akan menjadi non-blocking dan tidak akan menampilkan indikator pemuatan yang tidak diinginkan.
Returns
startTransition tidak mengembalikan apa pun.
Perhatian
-
useTransitionadalah sebuah Hook, sehingga hanya bisa dipanggil di dalam komponen atau Hook custom. Jika Anda ingin memulai sebuah transisi di tempat lain (contoh, dari data library), sebaiknya panggilstartTransitionsebagai gantinya. -
Anda dapat membungkus perubahan menjadi transisi hanya jika Anda memiliki akses pada fungsi
setpada state tersebut. Jika Anda ingin memulai sebuah transisi sebagai balasan dari beberapa prop atau nilai Hook custom, coba gunakanuseDeferredValuesebagai gantinya. -
Fungsi yang Anda teruskan ke
startTransitiondipanggil segera, menandai semua pembaruan status yang terjadi saat dijalankan sebagai Transisi. Jika Anda mencoba melakukan pembaruan status dalamsetTimeout, misalnya, pembaruan tersebut tidak akan ditandai sebagai Transisi. -
Anda harus membungkus pembaruan status apa pun setelah permintaan async apa pun dalam
startTransitionlain untuk menandainya sebagai Transisi. Ini adalah batasan yang diketahui yang akan kami perbaiki di masa mendatang (lihat Pemecahan Masalah). -
Fungsi
startTransitionmemiliki identitas yang stabil, jadi Anda akan sering melihatnya dihilangkan dari dependensi Efek, tetapi memasukkannya tidak akan menyebabkan Efek aktif. Jika linter memungkinkan Anda menghilangkan dependensi tanpa kesalahan, hal itu aman untuk dilakukan. Pelajari selengkapnya tentang menghapus dependensi Efek. -
Perubahan state yang ditandai sebagai transisi akan terganggu oleh perubahan state lainnya. Contohnya, jika anda mengubah komponen chart di dalam transisi, namun kemudian memulai mengetik dalam input ketika chart sedang di tengah merender ulang, React akan merender ulang pekerjaan pada komponen chart setelah mengerjakan perubahan pada input.
-
Perubahan transisi tidak dapat digunakan untuk mengontrol input teks.
-
Apabila terdapat beberapa transisi yang berjalan, React saat ini akan mengelompokkan mereka bersama. Ini adalah limitasi yang mungkin akan dihapus pada rilis yang akan datang.
Kegunaan
Melakukan pembaruan non-blocking dengan Aksi
Panggil useTransition pada level teratas komponen Anda untuk membuat Aksi, dan akses state tertunda:
import {useState, useTransition} from 'react';
function CheckoutForm() {
const [isPending, startTransition] = useTransition();
// ...
}useTransition mengembalikan sebuah senarai dengan tepat dua item:
- Penanda
isPendingyang memberitahukan Anda apakah terdapat transisi tertunda. - Fungsi
startTransitionyang memungkinkan Anda membuat Aksi.
Untuk memulai Transisi, oper sebuah fungsi ke startTransition seperti berikut:
import {useState, useTransition} from 'react';
import {updateQuantity} from './api';
function CheckoutForm() {
const [isPending, startTransition] = useTransition();
const [quantity, setQuantity] = useState(1);
function onSubmit(newQuantity) {
startTransition(async function () {
const savedQuantity = await updateQuantity(newQuantity);
startTransition(() => {
setQuantity(savedQuantity);
});
});
}
// ...
}Fungsi yang diteruskan ke startTransition disebut “Aksi”. Anda dapat memperbarui status dan (opsional) melakukan efek samping dalam sebuah Aksi, dan pekerjaan akan dilakukan di latar belakang tanpa menghalangi interaksi pengguna di halaman. Transisi dapat mencakup beberapa Aksi, dan saat Transisi sedang berlangsung, UI Anda tetap responsif. Misalnya, jika pengguna mengklik tab tetapi kemudian berubah pikiran dan mengklik tab lain, klik kedua akan segera ditangani tanpa menunggu pembaruan pertama selesai.
Untuk memberikan umpan balik kepada pengguna tentang Transisi yang sedang berlangsung, status isPending beralih ke true pada panggilan pertama ke startTransition, dan tetap true hingga semua Aksi selesai dan status akhir ditampilkan kepada pengguna. Transisi memastikan efek samping dalam Aksi selesai untuk mencegah indikator pemuatan yang tidak diinginkan, dan Anda dapat memberikan umpan balik langsung saat Transisi sedang berlangsung dengan useOptimistic.
Contoh 1 dari 2: Memperbarui kuantitas dalam suatu Aksi
Dalam contoh ini, fungsi updateQuantity mensimulasikan permintaan ke server untuk memperbarui jumlah barang di keranjang. Fungsi ini diperlambat secara artifisial sehingga butuh setidaknya satu detik untuk menyelesaikan permintaan.
Perbarui jumlah beberapa kali dengan cepat. Perhatikan bahwa status “Total” yang tertunda ditampilkan saat permintaan sedang berlangsung, dan “Total” diperbarui hanya setelah permintaan akhir selesai. Karena pembaruan berada dalam Aksi, “jumlah” dapat terus diperbarui saat permintaan sedang berlangsung.
import { useState, useTransition } from "react"; import { updateQuantity } from "./api"; import Item from "./Item"; import Total from "./Total"; export default function App({}) { const [quantity, setQuantity] = useState(1); const [isPending, startTransition] = useTransition(); const updateQuantityAction = async newQuantity => { // To access the pending state of a transition, // call startTransition again. startTransition(async () => { const savedQuantity = await updateQuantity(newQuantity); startTransition(() => { setQuantity(savedQuantity); }); }); }; return ( <div> <h1>Checkout</h1> <Item action={updateQuantityAction}/> <hr /> <Total quantity={quantity} isPending={isPending} /> </div> ); }
This is a basic example to demonstrate how Actions work, but this example does not handle requests completing out of order. When updating the quantity multiple times, it’s possible for the previous requests to finish after later requests causing the quantity to update out of order. This is a known limitation that we will fix in the future (see Troubleshooting below).
For common use cases, React provides built-in abstractions such as:
These solutions handle request ordering for you. When using Transitions to build your own custom hooks or libraries that manage async state transitions, you have greater control over the request ordering, but you must handle it yourself.
Mengekspos prop action dari komponen
Anda dapat mengekspos prop action dari sebuah komponen untuk memungkinkan komponen induk untuk memanggil Aksi.
Contohnya, komponen TabButton ini membungkus logika onClick di dalam prop action:
export default function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(async () => {
// await the action that's passed in.
// This allows it to be either sync or async.
await action();
});
}}>
{children}
</button>
);
}Karena komponen induk merubah state-nya di dalam action, perubahan state tersebut akan ditandai sebagai Transisi. Ini berarti Anda dapat menekan “Posts” dan kemudian segera menekan “Contact” dan ia tidak memblokir interaksi pengguna:
import { useTransition } from 'react'; export default function TabButton({ action, children, isActive }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } if (isPending) { return <b className="pending">{children}</b>; } return ( <button onClick={async () => { startTransition(async () => { // await the action that's passed in. // This allows it to be either sync or async. await action(); }); }}> {children} </button> ); }
Menampilan state visual tertunda
Anda dapat menggunakan nilai boolean isPending yang dikembalikan oleh useTransition untuk menandai ke pengguna bahwa transisi sedang berjalan. Contohnya, tombol tab dapat memiliki state visual special “pending”:
function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...Perhatikan bagaimana menekan “Posts” sekarang terasa lebih responsif karena tombol tab tersebut berubah langsung:
import { useTransition } from 'react'; export default function TabButton({ action, children, isActive }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } if (isPending) { return <b className="pending">{children}</b>; } return ( <button onClick={() => { startTransition(async () => { await action(); }); }}> {children} </button> ); }
Mencegah indikator loading yang tidak diinginkan
Pada contoh berikut ini, komponen PostsTab mengambil beberapa data menggunakan use. Ketika Anda menekan tab “Posts”, komponen PostsTab akan ditangguhkan, menyebabkan fallback loading terdekat untuk muncul:
import { Suspense, useState } from 'react'; import TabButton from './TabButton.js'; import AboutTab from './AboutTab.js'; import PostsTab from './PostsTab.js'; import ContactTab from './ContactTab.js'; export default function TabContainer() { const [tab, setTab] = useState('about'); return ( <Suspense fallback={<h1>🌀 Loading...</h1>}> <TabButton isActive={tab === 'about'} action={() => setTab('about')} > About </TabButton> <TabButton isActive={tab === 'posts'} action={() => setTab('posts')} > Posts </TabButton> <TabButton isActive={tab === 'contact'} action={() => setTab('contact')} > Contact </TabButton> <hr /> {tab === 'about' && <AboutTab />} {tab === 'posts' && <PostsTab />} {tab === 'contact' && <ContactTab />} </Suspense> ); }
Menyembunyikan seluruh kontainer tab untuk menampilkan indikator loading akan mengarahkan ke pengalaman pengguna yang gemuruh. Jika Anda menambahkan useTransition ke TabButton, Anda bisa sebagai gantinya mengindikasi tampilan state pending di tombol tab sebagai gantinya.
Perhatikan bahwa menekan “Posts” tidak menjadikan seluruh kontainer tab dengan spinner:
import { useTransition } from 'react'; export default function TabButton({ action, children, isActive }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } if (isPending) { return <b className="pending">{children}</b>; } return ( <button onClick={() => { startTransition(async () => { await action(); }); }}> {children} </button> ); }
Baca lebih lanjut tentang menggunakan transisi dengan Suspense.
Membangun router Suspense-enabled
Jika Anda membangun framework atau router React, kami merekomendasikan menandai navigasi halaman sebagai transisi.
function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...Ini direkomendasikan karena tiga alasan:
- Transisi dapat terputus, yang memungkinkan pengguna mengklik tanpa menunggu perenderan ulang selesai.
- Transisi mencegah indikator loading yang tidak diinginkan, yang memungkinkan pengguna menghindari lompatan menggelegar pada navigasi.
- Transisi menunggu semua tindakan yang tertunda yang memungkinkan pengguna menunggu efek samping selesai sebelum halaman baru ditampilkan.
Berikut adalah contoh router kecil sederhana menggunakan Transisi untuk navigasi.
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>; }
Menampilkan error ke pengguna dengan error boundary
If a function passed to startTransition throws an error, you can display an error to your user with an error boundary. To use an error boundary, wrap the component where you are calling the useTransition in an error boundary. Once the function passed to startTransition errors, the fallback for the error boundary will be displayed.
import { useTransition } from "react"; import { ErrorBoundary } from "react-error-boundary"; export function AddCommentContainer() { return ( <ErrorBoundary fallback={<p>⚠️Something went wrong</p>}> <AddCommentButton /> </ErrorBoundary> ); } function addComment(comment) { // Untuk tujuan demonstrasi untuk menunjukkan ErrorBoundary if (comment == null) { throw new Error("Example Error: An error thrown to trigger error boundary"); } } function AddCommentButton() { const [pending, startTransition] = useTransition(); return ( <button disabled={pending} onClick={() => { startTransition(() => { // Secara sengaja tidak menambahkan komentar // agar error ditampilkan addComment(); }); }} > Add comment </button> ); }
Pemecahan Masalah
Merubah input dalam transisi tidak bekerja
Anda tidak dapat menggunakan transisi unttuk variabel state yang mengendalikan input:
const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ Tidak dapat menggunakan transisi untuk state input terkontrol
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;Ini dikarenakan transisi adalah non-blocking, namun mengubah input dalam respon untuk mengubah event seharusnya bekerja secara sinkron. Jika Anda ingin menjalankan transisi sebagai respon untuk menulis, Anda memiliki dua opsi:
- Anda dapat mendeklarasikan dua variabel state berbeda: satu untuk state masukan ( yang selalu berubah secara sinkron), dan satu yang akan Anda ubah dalam transisi. Ini memungkinkan Anda mengendalikan masukan menggunakan state sinkron, dan mengirim variabel state transisi (yang akan “lag” dibelakang masukan) ke sisa logika rendering Anda.
- Kalau tidak, Anda dapat memiliki satu variabel state, dan tambahkan
useDeferredValueyang akan “lag” dibelakang nilai asli. Ini akan mentrigger merender ulang non-blocking untuk “mengejar” dengan nilai baru secara otomatis.
React tidak memperlakukan perubahan state saya sebagai transisi
Ketika Anda membungkus perubahan state di dalam transisi, pastikan bahwa itu terjadi saat memanggil startTransition:
startTransition(() => {
// ✅ Mengatur state *saat* startTransition dipanggil
setPage('/about');
});Fungsi yang Anda kirimkan ke startTransition harus sinkron. Anda tidak dapat menandakan perubahan sebagai transisi seperti berikut:
startTransition(() => {
// ❌ Mengatur state *setelah* startTransition dipanggil
setTimeout(() => {
setPage('/about');
}, 1000);
});Sebaiknya, anda dapat melakukan hal berikut:
setTimeout(() => {
startTransition(() => {
// ✅ Mengatur state *saat* startTransition dipanggil
setPage('/about');
});
}, 1000);React tidak memperlakukan pembaruan status saya setelah await sebagai Transisi
Bila Anda menggunakan await di dalam fungsi startTransition, pembaruan status yang terjadi setelah await tidak ditandai sebagai Transisi. Anda harus membungkus pembaruan status setelah setiap await dalam panggilan startTransition:
startTransition(async () => {
await someAsyncFunction();
// ❌ Tidak menggunakan startTransition setelah await
setPage('/about');
});Namun, ini bekerja sebagai gantinya:
startTransition(async () => {
await someAsyncFunction();
// ✅ Menggunakan startTransition *setelah* await
startTransition(() => {
setPage('/about');
});
});Ini adalah batasan JavaScript karena React kehilangan cakupan konteks async. Di masa mendatang, saat AsyncContext tersedia, batasan ini akan dihapus.
Saya ingin memanggil useTransition dari luar komponen
Anda tidak dapat memanggil useTransition di luar sebuah komponen karena ini adalah sebuah Hook. Dalam kasus ini, sebaiknya gunakanlah method startTransition. Itu bekerja dengan cara yang sama, namun itu tidak dapat memberikan indikator isPending.
Fungsi yang saya berikan ke startTransition tereksekusi langsung
Jika Anda menjalankan kode berikut, ini akan mencetak 1, 2, 3:
console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);Ini diharapkan untuk mencetak 1, 2, 3. Fungsi yang Anda berikan ke startTransition tidak tertunda. Tidak seperti milik browser setTimeout, hal tersebut nantinya tidak menjalankan callback. React akan eksekusi fungsi Anda secara langsung, namun perubahan state yang terjadwal saat berjalan akan ditandai sebagai transisi. Anda dapat membayangkan hal tersebut bekerja seperti berikut:
// A simplified version of how React works
let isInsideTransition = false;
function startTransition(scope) {
isInsideTransition = true;
scope();
isInsideTransition = false;
}
function setState() {
if (isInsideTransition) {
// ... schedule a Transition state update ...
} else {
// ... schedule an urgent state update ...
}
}My state updates in Transitions are out of order
If you await inside startTransition, you might see the updates happen out of order.
In this example, the updateQuantity function simulates a request to the server to update the item’s quantity in the cart. This function artificially returns the every other request after the previous to simulate race conditions for network requests.
Try updating the quantity once, then update it quickly multiple times. You might see the incorrect total:
import { useState, useTransition } from "react"; import { updateQuantity } from "./api"; import Item from "./Item"; import Total from "./Total"; export default function App({}) { const [quantity, setQuantity] = useState(1); const [isPending, startTransition] = useTransition(); // Store the actual quantity in separate state to show the mismatch. const [clientQuantity, setClientQuantity] = useState(1); const updateQuantityAction = newQuantity => { setClientQuantity(newQuantity); // Access the pending state of the transition, // by wrapping in startTransition again. startTransition(async () => { const savedQuantity = await updateQuantity(newQuantity); startTransition(() => { setQuantity(savedQuantity); }); }); }; return ( <div> <h1>Checkout</h1> <Item action={updateQuantityAction}/> <hr /> <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} /> </div> ); }
When clicking multiple times, it’s possible for previous requests to finish after later requests. When this happens, React currently has no way to know the intended order. This is because the updates are scheduled asynchronously, and React loses context of the order across the async boundary.
This is expected, because Actions within a Transition do not guarantee execution order. For common use cases, React provides higher-level abstractions like useActionState and <form> actions that handle ordering for you. For advanced use cases, you’ll need to implement your own queuing and abort logic to handle this.
Example of useActionState handling execution order:
import { useState, useActionState } from "react"; import { updateQuantity } from "./api"; import Item from "./Item"; import Total from "./Total"; export default function App({}) { // Store the actual quantity in separate state to show the mismatch. const [clientQuantity, setClientQuantity] = useState(1); const [quantity, updateQuantityAction, isPending] = useActionState( async (prevState, payload) => { setClientQuantity(payload); const savedQuantity = await updateQuantity(payload); return savedQuantity; // Return the new quantity to update the state }, 1 // Initial quantity ); return ( <div> <h1>Checkout</h1> <Item action={updateQuantityAction}/> <hr /> <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} /> </div> ); }