useTransition

useTransition adalah sebuah React Hook yang memungkinkan Anda merubah suatu state tanpa memblokir UI.

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.

fungsi startTransition

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

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 kirimkan kepada startTransition haruslah sinkron. React akan langsung mengeksekusi fungsi ini, menandai semua perubahan state yang terjadi sambil mengeksekusinya sebagai transisi. Jika Anda mencoba untuk melakukan perubahan state lebih nanti (contoh, saat timeout), mereka tidak akan ditandai sebagai transisi.

  • 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

Menandai perubahan state sebagai transisi non-blocking

Panggil useTransition pada level teratas komponen Anda untuk menandai perubahan state sebagai transisi non-blocking.

import { useState, useTransition } from 'react';

function TabContainer() {
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 menandai perubahan state sebagai transisi.

Kemudian Anda dapat menandai perubahan state sebagai transisi seperti berikut:

function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');

function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}

Transisi akan memungkinkan Anda untuk mempertahankan perubahan tampilan pengguna secara responsif bahkan untuk perangkat lambat.

Dengan transisi, UI Anda akan tetap responsif di tengah-tengah me-render ulang. Contohnya, jika pengguna menekan tab namun mereka berubah pikiran dan menekan tab lain, mereka dapat melakukan itu tanpa menunggu muat ulang pertama selesai.

Perbedaan antara useTransition dan perubahan state biasa

Contoh 1 dari 2:
Merubah tab saat ini dalam transisi

Pada contoh berikut ini, tab “Posts” ini Dipelankan secara artifisial sehingga akan memakan waktu setidaknya satu detik untuk render.

Tekan “Posts” kemudian segera tekan “Contact”. Perhatikan bahwa ini akan mengganggu muatan “Posts” yang lambat. Tab “Contact” akan tampil segera. Karena perubahan state ini ditandai sebagai transisi, merender ulang yang lambat tidak akan membekukan tampilan pengguna.

import { useState, useTransition } 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 [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => selectTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => selectTab('posts')}
      >
        Posts (slow)
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => selectTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}


Merubah komponen induk dalam transisi

Anda dapat mengubah state komponen induk dari panggilan useTransition juga. Contohnya, komponen TabButton ini membungkus logika komponen onClick dalam sebuah transisi:

export default function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(() => {
onClick();
});
}}>
{children}
</button>
);
}

Karena komponen induk merubah statenya di dalam event handler onClick, perubahan state tersebut akan ditandai sebagai transisi. Inilah mengapa, seperti pada contoh di awal, Anda dapat menekan pada “Posts” dan kemudian segera menekan “Contact”. Mengubah tab yang dipilih akan ditandai sebagai transisi, sehingga itu tidak memblokir tampilan pengguna.

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}


Menampilan state visual tertunda saat transisi

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({ children, isActive, onClick }) {
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({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}


Mencegah indikator loading yang tidak diinginkan

Pada contoh berikut ini, komponen PostsTab mengambil beberapa data menggunakan Suspense-enabled data source. Ketika Anda menekan tab “Posts”, komponen PostsTab akan disuspends, 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'}
        onClick={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => setTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </Suspense>
  );
}

Menyembunyikan seluruh tab container 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 tab container dengan spinner:

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}

Baca lebih lanjut tentang menggunakan transisi dengan Suspense.

Catatan

Transisi hanya akan “menunggu” cukup lama untuk menghindari konten already revealed (seperti tab container). Jika tab Posts memiliki nested <Suspense> boundary, transisi tidak akan “menunggu” untuk itu.


Membangun router Suspense-enabled

Jika Anda membangun React framework atau router, 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 dua 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

Canary

Error Boundary untuk useTransition saat ini hanya tersedia di kanal canary dan experimental React. Pelajari lebih lanjut tentang kanal rilis React di sini.

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

Demikian pula, Anda tidak dapat menandai perubahan sebagai transisi seperti berikut:

startTransition(async () => {
await someAsyncFunction();
// ❌ Mengatur state *setelah* startTransition dipanggil
setPage('/about');
});

Namun, ini bekerja sebagai gantinya:

await someAsyncFunction();
startTransition(() => {
// ✅ Mengatur state *saat* startTransition dipanggil
setPage('/about');
});

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