useTransition

useTransition adalah sebuah React Hook yang memungkinkan Anda merender sebagian UI di latat belakang.

const [isPending, startTransition] = useTransition()

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:

  1. Penanda isPending yang memberitahukan Anda bahwa terdapat transisi yang tertunda.
  2. 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);
});
}
// ...
}

Catatan

Functions called in startTransition are called “Actions”.

The function passed to startTransition is called an “Action”. By convention, any callback called inside startTransition (such as a callback prop) should be named action or include the “Action” suffix:

function SubmitButton({ submitAction }) {
const [isPending, startTransition] = useTransition();

return (
<button
disabled={isPending}
onClick={() => {
startTransition(async () => {
await submitAction();
});
}}
>
Submit
</button>
);
}

Parameters

  • action: Fungsi yang memperbarui suatu state dengan memanggil satu atau beberapa fungsi set. React memanggil action segera tanpa parameter dan menandai semua pembaruan state yang dijadwalkan secara sinkron selama panggilan fungsi action sebagai Transisi. Semua panggilan asinkron yang ditunggu dalam action akan disertakan dalam Transisi, tetapi saat ini memerlukan pembungkusan semua fungsi set setelah await dalam startTransition 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 panggil startTransition 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 gunakan useDeferredValue 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 dalam setTimeout, 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:

  1. Penanda isPending yang memberitahukan Anda apakah terdapat transisi tertunda.
  2. 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.

Perbedaan antara Aksi dan penanganan event reguler

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>
  );
}

Catatan

When exposing an action prop from a component, you should await it inside the transition.

This allows the action callback to be either synchronous or asynchronous without requiring an additional startTransition to wrap the await in the action.


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.

Catatan

Transisi hanya akan “menunggu” cukup lama untuk menghindari konten yang telah ditampilkan (seperti kontainer tab). Jika tab Posts memiliki batasan <Suspense> bersarang, Transisi tidak akan “menunggu” untuk itu.


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:

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>;
}

Catatan

Secara default, router Suspense-enabled diharapkan untuk membungkus perubahan navigasi menjadi transisi.


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:

  1. 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.
  2. 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>
  );
}