Technologie

Future of React

05. 08. 2021

Intro

Před několika týdny byla oznámena nová alfa verze React 18. Než si všechny novinky nahodíme do produkčních projektů, pojďme se podívat, jak budeme React používat v budoucnu. Ukážeme si zejména princip concurrency modu, resp. některých jeho featur jako je Suspense a Concurrency UI Patterns. Na nových funkcích Reactu je pěkně vidět, jak jde naproti implementaci skutečně robustních systémů a usnadňuje nám každodenní práci.  

Suspense

Technologie React suspense už se nedá řadit mezi future, jelikož je již nějakou dobu v Reactu k dispozici. 

Suspense umožňuje komponentě si na něco počkat, než přijde render. V aktuální verzi Reactu lze Suspense využít pouze k jednomu účelu – dynamické načítání komponent pomocí React.lazy:

const DynamicComponent = React.lazy(() => import(‘./DynamicComponent’));

<Suspense fallback=”Loading”>
  <DynamicComponent />
</Suspense>

V tomto UI uvidíte text “Loading” dokud nedojde k načtení komponenty, ze které se stal async modul po použití React.lazy.

Suspense for data

Ve většině aplikací chceme asynchronně načítat komponenty a zároveň data. Velmi často je UI implementovano tak, že se zobrazuje indikátor načítání komponent (async modulu) a poté při renderu se začnou načítat data a zobrazuje se indikátor zase jiný, což způsobuje nepříjemné “blikání” obrazovky. Uživatele ale vůbec nezajímá, jestli načítáme data či komponentu. 

Pojďme si nyní vysvětlit pozadí Suspense technologie a uvidíme, jestli ji dokážeme aplikovat jak na načtení komponent i  na načtení dat a zachovat co nejlepší uživatelský zážitek.

Většina z nás asi ví, že lze v JavaScriptu “vyhodit” nějakou chybu:

throw new Error(“Ooops!”)

Méně známý je fakt, že můžeme “vyhodit” v podstatě cokoliv – číslo, text, objekt..

throw “Suspense”
throw 1
throw new Promise()

A právě poslední příklad s Promise je pro nás důležitý, jelikož async. komponenty i načítání dat je na Promise založeno.

Pomocí zmíněného principu je právě implementován suspense. Stačí kdekoliv uvnitř komponenty Suspense vyhodit Promise, který je zachycen a než je vyhodnocen, tak komponenta zobrazuje fallback.

// file: resource.js
// implementace velmi primitivní “knihovny” na načítání dat
export const initResource = () => {
  let data;
  let status = ‘pending’;’ 
   
// DŮLEŽITÉ - zde načítáme data ihned při inicializaci, co nejdříve

  // budou tedy připravena ke čtení až bude třeba 

  const promise = fetch(‘/data’).then((res) => {
    data = response;
    status = ‘done’;
  })

  const read() {
    if (status === ‘pending’) {
      throw promise;
    } else {
      return data;
    }
  }
  return { read };
}


// DataComponent.js
const resource = initResource();

export const DataComponent = () => {
  // pokud promise neskoncil, “vyhodi se” a je odchycen pomoci Suspense (viz. resource.js, funkce read)
  const data = resource.read();  
  return (
    <div>
      {data.example}
    </div>
  )
}

// Root.js
const DynamicComponent = React.lazy(() => import(‘./DynamicComponent’));

const App = () => (
  <Suspense fallback=”Loading”>
    <DynamicComponent />
  </Suspense>
);

Kromě toho, že používáme jednu komponentu jako placeholder při načítání (pro data i komponenty) tak jste si mohli povšimnout ještě jedné zajímavé featury, kterou nám implementace nabízí.

Ve většině případů spouštíme načítání dat uvnitř komponenty (componentDidMount, useEffect). V našem případě spouštíme načítání dat při inicializaci resource. A to se děje dříve, než render začne, je to tedy i jakési vylepšení v rychlosti spouštění requestu. V komponentě se poté snažíme z resource data číst pomocí funkce read, která buď vrátí data nebo vyhodí Promise a způsobí zobrazení fallbacku.

Concurrency UI

Klasicky se snažíme při načítání čehokoliv zobrazit nějaké indikátory načítání. Občas se “bohužel” stane, že máme rychlý backend a vše se načte dost rychle, např. do 200ms. V takovém případě dojde v UI k probliknutí, kdy se zobrazí indikátor načítání na velmi krátký čas. Z UX pohledu to působí spíše rušivě než informativně. Proto bych chtěl mít možnost si tak trošku pozastavit render, aby aplikace zbytečně neproblikávala.

Přesně této potřebě jde React naproti s implementací Concurrent UI Patterns. Shodou okolností byla před pár týdny oznámena nová verze React 18 (zatím alpha), ve které se konečně dočkáme podpory concurrency transitions.

Concurrency UI Patterns nám umožní prioritizovat určité části kódu a s renderem počkat na později. Konkrétní rozhraní vystavené Reactem bude v podobě např. useTransition hooku (ale již existují i další) a právě jeho použití si zde ukážeme a pokusíme se vylepšit uživatelský zážitek z používání aplikace.

Předpokládejme, že načítáme data s jedním parametrem, například číslo stránky a chceme implementovat přepínání stránek.

Následující implementace nám zajistí, že cokoliv uvnitř startTransition callbacku se provede a render je pozdržen po dobu námi nastavenou (timeout argument useTransition hooku). Tedy pokud vše proběhne do 300ms uživatel uvidí rovnou nová data bez blikání. Uživatelské rozhraní se prostě na chvíli “zastaví”, ale pokud je to dostatečně krátká doba, uživatel spíše ocení, že rovnou vidí data která potřebuje, než problikávání různých točících koleček. Pokud se načítání protáhne tak žádný problém, dáme uživateli vědět, že se data ještě načítají. React nám opět velmi pomáhá a informuje nás o stavu načítání pomocí proměnné “isPending” v kódu níže.

// DataComponent.js
const initialResource = initResource(1);

export const DataComponent = () => {
  const [resource, setResource] = useState(initialResource);
  const [startTransition, isPending] = useTransition({
    timeoutMs: 300,
  })
  const data = resource.read();  

  return (
    <div>
      {data.example}
    <button
      onClick={() => {
        startTransition(() => {
          setResource(initResrouce(2))
        }}
      }}
    >
     Load 2. page
    </button>
    </div>
  )
}

Kompletní použití všech vychytávek je možno najít ve spustitelné demo aplikace zde: https://github.com/adamryvola/suspense-data-demo

Další články

Zobrazit všechno