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
await
sebagai Transisi - Saya ingin memanggil
useTransition
dari luar komponen - Fungsi yang saya berikan ke
startTransition
tereksekusi 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
isPending
yang memberitahukan Anda bahwa terdapat transisi yang tertunda. - Fungsi
startTransition
yang 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 memanggilaction
segera tanpa parameter dan menandai semua pembaruan state yang dijadwalkan secara sinkron selama panggilan fungsiaction
sebagai Transisi. Semua panggilan asinkron yang ditunggu dalamaction
akan disertakan dalam Transisi, tetapi saat ini memerlukan pembungkusan semua fungsiset
setelahawait
dalamstartTransition
tambahan (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
-
useTransition
adalah 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 panggilstartTransition
sebagai gantinya. -
Anda dapat membungkus perubahan menjadi transisi hanya jika Anda memiliki akses pada fungsi
set
pada state tersebut. Jika Anda ingin memulai sebuah transisi sebagai balasan dari beberapa prop atau nilai Hook custom, coba gunakanuseDeferredValue
sebagai gantinya. -
Fungsi yang Anda teruskan ke
startTransition
dipanggil 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
startTransition
lain untuk menandainya sebagai Transisi. Ini adalah batasan yang diketahui yang akan kami perbaiki di masa mendatang (lihat Pemecahan Masalah). -
Fungsi
startTransition
memiliki 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
isPending
yang memberitahukan Anda apakah terdapat transisi tertunda. - Fungsi
startTransition
yang 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
useDeferredValue
yang 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> ); }