Přiznám se, že se na nadcházející verzi reactu opravdu těším. Přináší totiž řekněme nový pattern pro práci s asynchronními stavy aplikace. Troufám si říct, že je docela elegantní a vcelku jednoduchý. A navíc otevírá dosud nevídané možnosti.

Jak jsme si minule ukázali, udržet konzistentní stav v asynchronní reactové aplikaci skýtá mnohé nástrahy a není úplně jednoduché všechno správně ošetřit a problémům tak včas předejít. Největší bolestí je pomyslné oddělení samotné exekuce a synchronizace jejího stavu se zbytkem světa. Tím myslím takové to známé a hodně ukecané:

const initialState = {
  id: null,
  isLoading: false,
  error: null,
  data: null,
}

const reducer = (state, action) => {
  if (action.type === 'START')
    return { ...state, id: action.id, isLoading: true }
  if (action.type === 'END' && state.id === action.id)
    return { ...state, data: action.data, isLoading: false }
  ...
  return state
}

Pokud máte štěstí na jazyk s typy a pattern matchingem a nutkání dělat věci lépe, pak to můžete vylepšit o bezpečnější reprezentaci a předejít tak neexistujícím stavům.

Nebylo by lepší ve stavu aplikace (nebo komponenty) rovnou držet něco, co může nabývat těchto tří stavů (pending, resolved a rejected) a předejít tak téhle zrcadlové hře?

A to je přesně ten nový pattern, kterým react posvěcuje ukládat do stavu promisu. Teda ne úplně promisu, ale resource.

Ve zkratce jde o to, že resource se může začít “resolvovat” hned jakmile dojde k jeho vytvoření (stejně jako promisa). Po nějaké době dojde k jeho dokončení a skončí buď ve stavu “resolved” a drží tak například načtená data (stejně jako promisa), nebo skončí chybou ve stavu “rejected” (stejně jako promisa).

Důležitý rozdíl mezi promisou a resourcem je ten, že resource v případě “pending” stavu vyhazuje sám sebe. Pro lepší pochopení kód:

function createResource(promise) {
  const [rejected, resolved, pending] = [0, 1, 2]

  let state = [pending, undefined]

  promise
    .then((data) => { state = [resolved, data] })
    .catch((error) => { state = [rejected, error] })

  return {
    read() {
      const [status, result] = state
      if (status == pending) throw promise
      if (status == rejected) throw result
      return result
    }
  }
}

const createRecipeResource = (id) => createResource(asyncFetch(id))

Díky tomu vyhození sám sebe (throw promise) se může kdokoliv z okolního světa na tento “pending” resource pověsit a reagovat až přesně v momentě, kdy dojde k jeho dokončení. Toho využívá právě react, respektive nová komponenta Suspense (případně SuspenseList), která je schopná takový resource zachytit a po dobu jeho “resolvování” zobrazit fallback.

const RecipeDetail = ({ resource }) => {
  const data = resource.read()
  return <h1>{data.title}</h1>
}

const App = () => {
  const [recipe] = useState(
    () => createRecipeResouce("fish-soup")
  )
  return (
    <Suspense fallback="loading...">
      <RecipeDetail resource={recipe}>
    </Suspense>
  )
}

Stejně jako v případě promisy, tak i resource může nabývat “rejected” stavu. To se pak řeší již známou komponentou ErrorBoundary. Jak Suspense, tak ErrorBoundary fungují úplně stejně jako konstrukt try/catch, což znamená, že nemusíte pending/rejected resource ošetřovat v místě použití, ale kdekoliv výše, což v tomto případě zlepšuje kompozici.

Nejdůležitější přínos této abstrakce je ale v odemčení možností pro vytváření lepšího UI/UX za menší cenu.

V kombinaci s konkurentním reactem a novým hookem nazvaným useTransition lze například na X milisekund pozdržet přeměnu UI ze stavu A do stavu B a využít této mezery pro načítání. Stihne-li se vše načíst (díky tomu vyhodí“sám seb”) do X milisekund, pak uživatel dostane stav B bez jakýchkoliv blikajících a tedy rušivých loadingů. Naopak na pomalé síti se loadingy po X milisekundách zobrazí a uživatel tak dostane jasnou informaci, že se něco děje a vše je v pořádku. A přitom tu nejtěžší práci, orchestraci, synchronizaci a celkovou konzistenci stavů zajistí sám react, nikoliv mnohdy fragilní, narychlo spíchnutý custom kód… A tomu já dám vždycky přednost.