useDeferredValue

useDeferredValue je React Hook koji vam omogućava da odložite ažuriranje dela UI-a.

const deferredValue = useDeferredValue(value)

Reference

useDeferredValue(value, initialValue?)

Pozovite useDeferredValue na vrhu vaše komponente da dobijete odloženu verziju te vrednosti.

import { useState, useDeferredValue } from 'react';

function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}

Pogledajte još primera ispod.

Parametri

  • value: Vrednost koju želite da odložite. Može imati bilo koji tip.
  • opcioni initialValue: Vrednost koja se koristi prilikom inicijalnog rendera komponente. Ako se izostavi, useDeferredValue neće odložiti tokom inicijalnog rendera, jer ne postoji prethodna vrednost value-a koja može biti renderovana.

Povratne vrednosti

  • currentValue: Tokom inicijalnog rendera, povratna odložena vrednost će biti initialValue ili ista kao prosleđena vrednost. Tokom ažuriranja, React će prvo pokušati ponovno renderovanje sa starom vrednošću (znači da će vratiti staru vrednost), a onda pokušati drugi ponovni render u pozadini sa novom vrednošću (znači da će vratiti ažuriranu vrednost).

Upozorenja

  • Kada je ažuriranje unutar Transition-a, useDeferredValue uvek vraća novi value i ne stvara odloženi render, pošto je ažuriranje već odloženo.

  • Vrednosti koje prosleđujete u useDeferredValue bi trebale biti ili primitivne (poput stringova i brojeva) ili objekti kreirani izvan renderovanja. Ako kreirate novi objekat tokom renderovanja i odmah ga prosledite u useDeferredValue, biće drugačiji u svakom renderu, stvarajući nepotrebne ponovne rendere u pozadini.

  • Kada useDeferredValue primi drugačiju vrednost (upoređenu sa Object.is), kao dodatak na trenutni render (koji još uvek koristi prethodnu vrednost), zakazaće se ponovni render u pozadini sa novom vrednošću. Ponovni render u pozadini može biti prekinut: ako postoji drugo ažuriranje value-a, React će restartovati ponovni render u pozadini. Na primer, ako korisnik brže kuca u input nego što se tabela koja prima odloženu vrednost može ponovo renderovati, tabela će se ponovo renderovati tek kada korisnik prestane sa kucanjem.

  • useDeferredValue je integrisan sa <Suspense>-om. Ako pozadinsko ažuriranje prouzrokovano novom vrednošću suspenduje UI, korisnik neće videti fallback. Videće samo staru odloženu vrednost dok se podaci ne učitaju.

  • useDeferredValue sam od sebe ne sprečava dodatne mrežne zahteve.

  • Ne postoji fiksno kašnjenje koje prouzrokuje useDeferredValue. Čim React završi originalni ponovni render, odmah će početi da radi na ponovnom renderu u pozadini sa novom odloženom vrednošću. Bilo koja ažuriranja prouzrokovana event-ovima (poput pisanja) će dobiti prioritet i prekinuti ponovni render u pozadini.

  • Ponovni render u pozadini prouzrokovan useDeferredValue-om neće okinuti Effect-e dok ne bude završen. Ako se ponovni render u pozadini suspenduje, Effect-i će se pokrenuti nakon što se podaci učitaju i UI ažurira.


Upotreba

Prikazivanje zastarelog sadržaja dok se novi sadržaj učitava

Pozovite useDeferredValue na vrhu vaše komponente da odložite ažuriranje nekog dela UI-a.

import { useState, useDeferredValue } from 'react';

function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}

Tokom inicijalnog rendera, odložena vrednost će biti ista kao i vrednost koju ste prosledili.

Tokom ažuriranja, odložena vrednost će “lag-ovati” za najnovijom vrednošću. Konkretno, React će prvo ponovo renderovati bez ažuriranja odložene vrednosti, a onda pokušati da ponovo renderuje u pozadini sa novodobijenom vrednošću.

Prođimo kroz primer da vidite kada je ovo korisno.

Napomena

Ovaj primer očekuje da koristite izvor podataka koji podržava Suspense:

  • Fetch-ovanje podataka sa framework-ovima koji podržavaju Suspense, npr. Relay i Next.js
  • Lazy-loading koda komponente sa lazy
  • Čitanje vrednosti Promise-a sa use

Naučite više o Suspense-u i njegovim ograničenjima.

U ovom primeru, SearchResults komponenta se suspenduje dok fetch-uje rezultate pretrage. Probajte ukucati "a", sačekati rezultate, a onda promeniti na "ab". Rezultati za "a" će biti zamenjeni sa fallback-om za učitavanje.

import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  return (
    <>
      <label>
        Pretraži albume:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Učitavanje...</h2>}>
        <SearchResults query={query} />
      </Suspense>
    </>
  );
}

Uobičajeni alternativni UI šablon je odlaganje ažuriranja liste rezultata i prikazivanje prethodnih rezultata dok novi rezultati ne budu spremni. Pozovite useDeferredValue da prosledite odloženu vrednost query-ja:

export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Pretraži albume:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Učitavanje...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}

query će se odmah ažurirati, pa će input prikazati novu vrednost. Međutim, deferredQuery će zadržati njegovu prethodnu vrednost dok se podaci ne učitaju, pa će SearchResults na trenutak prikazivati zastarele rezultate.

Unestite "a" u primer ispod, sačekajte da se rezultati učitaju, a onda promenite na "ab". Primetite kako sad, umesto Suspense fallback-a, vidite zastarelu listu rezultata dok se novi rezultati ne učitaju:

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Pretraži albume:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Učitavanje...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

Deep Dive

Kako odlaganje vrednosti radi ispod haube?

Možete zamisliti da se to dešava u dva koraka:

  1. Prvo, React ponovo renderuje sa novom query vrednošću ("ab"), ali sa starom deferredQuery vrednošću (još uvek "a"). deferredQuery vrednost, koju prosleđujete u listu rezultata, je odložena: “lag-uje za” query vrednošću.

  2. U pozadini React pokušava da ponovo renderuje i query i deferredQuery ažuriran na "ab". Ako se ovaj ponovni render završi, React će ga prikazati na ekranu. Međutim, ako se suspenduje (rezultati za "ab" se još nisu učitali), React će odbaciti pokušaj renderovanja i ponovo pokušati ovaj ponovni render kad se podaci učitaju. Korisnik će videti zastarelu odloženu vrednost sve dok podaci ne budu spremni.

Odloženo “pozadinsko” renderovanje može biti prekinuto. Na primer, ako ponovo kucate u input, React će ga odbaciti i restartovati sa novom vrednošću. React će uvek koristiti poslednju dostupnu vrednost.

Primetite da i dalje postoji mrežni zahtev pri svakom pritisku tastera na tastaturi. Ono što je ovde odloženo je prikaz rezultata (dok ne budu spremni), a ne sami mrežni zahtevi. Čak iako korisnik nastavi da kuca, odgovori za svaki taster će biti keširani, pa je pritiskanje Backspace-a trenutno i ne fetch-uje podatke ponovo.


Indikacija da je sadržaj zastareo

U primeru iznad, ništa nije ukazivalo da se lista rezultata za najnoviji upit još uvek učitava. Ovo može biti zbunjujuće za korisnika ako novim rezultatima treba vremena da se učitaju. Da bi korisniku bilo očiglednije da lista rezultata ne odgovara najnovijem upitu, možete dodati vizuelnu indikaciju da je prikazana zastarela lista rezultata:

<div style={{
opacity: query !== deferredQuery ? 0.5 : 1,
}}>
<SearchResults query={deferredQuery} />
</div>

Sa ovom promenom, čim krenete kucati, zastarela lista rezultata će postati pomalo zamagljena dok se nova lista rezultata ne učita. Takođe možete dodati i CSS transition za postepeno zamagljivanje, kao u primeru ispod:

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  return (
    <>
      <label>
        Pretraži albume:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Učitavanje...</h2>}>
        <div style={{
          opacity: isStale ? 0.5 : 1,
          transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear'
        }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </>
  );
}


Odlaganje ponovnog rendera za deo UI-a

Takođe, možete primeniti useDeferredValue kao optimizaciju performansi. Korisno je kada je deo vašeg UI-a spor prilikom ponovnog rendera, ne postoji lak način da ga optimizujete, a želite da ga sprečite da blokira ostatak UI-a.

Zamislite da imate tekstualno polje i komponentu (poput tabele ili dugačke liste) koja se ponovo renderuje pri svakom pritisku tastera na tastaturi:

function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}

Prvo, optimizujte SlowList da preskoči ponovne rendere kada su joj props-i jednaki. Da biste to uradili, obmotajte je sa memo:

const SlowList = memo(function SlowList({ text }) {
// ...
});

Međutim, ovo pomaže samo kada su props-i SlowList-a jednaki kao u prethodnom renderu. Problem koji sada imate je da je sporo kada su props-i drugačiji i kada zapravo trebate prikazati drugačiji vizuelni prikaz.

Konkretno, glavni problem sa performansama je taj da kad god nešto kucate u input, SlowList prima nove props-e, a ponovno renderovanje celokupnog stabla čini kucanje nezgodnim. U ovom slučaju, useDeferredValue vam omogućava da date prioritet ažuriranju input-a (koje mora biti brzo) u odnosu na ažuriranje liste rezultata (što je dozvoljeno da bude sporije):

function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}

Ovo ne čini ponovno renderovanje SlowList-a bržim. Međutim, govori React-u da ponovno renderovanje liste ima manji prioritet, tako da neće blokirati unos sa tastature. Lista će “lag-ovati za” input-om i onda ga “sustići”. Kao i pre, React će pokušati da ažurira listu što je pre moguće, ali neće blokirati korisnika prilikom kucanja.

Razlika između upotrebe useDeferredValue i neoptimizovanog ponovnog rendera

Primer 1 od 2:
Odloženo ponovno renderovanje liste

U ovom primeru, svaka stavka u SlowList komponenti je veštački usporena kako biste videli da useDeferredValue omogućava da input ostane responzivan. Kucajte u input i primetite da kucanje deluje brzo, iako lista “lag-uje za” njim.

import { useState, useDeferredValue } from 'react';
import SlowList from './SlowList.js';

export default function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={deferredText} />
    </>
  );
}

Pitfall

Ova optimizacija zahteva da je SlowList obmotan sa memo. To je zbog toga što kad god se text promeni, React mora biti sposoban da brzo ponovo renderuje roditeljsku komponentu. Tokom tog ponovnog rendera, deferredText i dalje ima prethodnu vrednost, pa SlowList može da preskoči ponovno renderovanje (props-i joj se nisu promenili). Bez memo bi svakako morala da se ponovo renderuje, čime bi se izgubio smisao optimizacije.

Deep Dive

Kako je odlaganje vrednosti različito od debouncing-a i throttling-a?

Postoje dve uobičajene tehnike optimizacije koje ste možda ranije koristili u ovakvim slučajevima:

  • Debouncing znači da ćete čekati korisnika da prestane sa kucanjem (npr. na sekund) pre ažuriranja liste.
  • Throttling znači da ćete ažurirati listu svako malo (npr. najviše jednom u sekundi).

Iako su ove tehnike korisne u nekim slučajevima, useDeferredValue je bolji za optimizaciju renderovanja jer je duboko integrisan sa samim React-om i prilagođen uređaju korisnika.

Za razliku od debouncing-a ili throttling-a, ne zahteva odabir fiksnog kašnjenja. Ako je uređaj korisnika brz (npr. moćan laptop), odloženi ponovni render se dešava gotovo odmah i neće biti primetan. Ako je uređaj korisnika spor, lista će “lag-ovati za” input-om proporcionalno u odnosu na to koliko je uređaj spor.

Takođe, za razliku od debouncing-a ili throttling-a, odloženi ponovni renderi sa useDeferredValue se, po default-u, mogu prekinuti. To znači da, ako je React u sred ponovnog renderovanja velike liste, a korisnik pritisne neki taster, React će odbaciti taj ponovni render, obraditi pritisak tastera, i tek onda ponovo započeti renderovanje u pozadini. U poređenju sa tim, debouncing i throttling i dalje pružaju nezgodno iskustvo jer su blokirajući: oni samo odlažu momenat kada renderovanje blokira pritiskanje tastera.

Ako se posao koji optimizujete ne dešava tokom renderovanja, debouncing i throttling su i dalje korisni. Na primer, mogu vam omogućiti da pravite manje mrežnih zahteva. Takođe, ove tehnike možete koristiti zajedno.