Cesta za lepším UI
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 a využít sílu kompozice.
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.