Tags : #Développement web #Développeur #Librairie #Open-source #Projet
Pourquoi a t’on créé ui-state, une librairie TypeScript de gestion d’états ? Tout viens de la lecture d’un très bon article de Dominic Dorfmeister aka TkDodo (on vous conseille aussi de lire ses autres articles sur son blog)
Dans l’article Component Composition is great btw, TkDodo met en lumière un problème récurrent : gérer les états d’une UI (loading, error, empty, success, etc.) de façon lisible, maintenable et typée… sans exploser la structure de son composant.
Prenons le point de départ typique.
On écrit un composant simple :
export function ShoppingList() {
const { data, isPending } = useQuery(/* ... */)
return (
<Card>
<CardHeading>Welcome 👋</CardHeading>
<CardContent>
{data?.assignee ? <UserInfo {...data.assignee} /> : null}
{isPending ? <Skeleton /> : null}
{data
? data.content.map((item) => (
<ShoppingItem key={item.id} {...item} />
))
: null}
</CardContent>
</Card>
)
}
Et là, en surface, tout semble « fonctionner ».
Mais très vite, ça devient flou :
data et isPending en même temps ?data signifie une erreur ou une liste vide ?data est présent mais vide ?On se retrouve à jongler entre plusieurs flags (isPending, data, isError, etc.) qui peuvent potentiellement faire que deux morceaux de l’UI s’affichent en même temps alors que ce n’était pas ce qu’on avait prévu.
C’est difficile à lire, à tester et à maintenir.
TkDodo propose alors un refacto plus explicite, basé sur des early return :
function Layout(props: { children: ReactNode; title?: string }) {
return (
<Card>
<CardHeading>Welcome 👋 {props.title}</CardHeading>
<CardContent>{props.children}</CardContent>
</Card>
)
}
export function ShoppingList() {
const { data, isPending } = useQuery(/* ... */)
if (isPending) {
return (
<Layout>
<Skeleton />
</Layout>
)
}
if (!data) {
return (
<Layout>
<EmptyScreen />
</Layout>
)
}
return (
<Layout title={data.title}>
{data.assignee ? <UserInfo {...data.assignee} /> : null}
{data.content.map((item) => (
<ShoppingItem key={item.id} {...item} />
))}
</Layout>
)
}
C’est beaucoup plus clair. À chaque état correspond un rendu.
Mais ça vient avec une contrepartie : on doit extraire le layout dans un composant, et que faire si on ne veut pas que tout l’écran change ? Le composant Layout est dupliqué dans chaque branche. On doit extraire le typage pour pouvoir typer les props du Layout. Et si on veut qu’une partie de l’interface reste constante entre les états (par exemple un header ou une sidebar) ou si une ou plusieurs parties du Layout sont dépendantes de l’état, on doit commencer à restructurer son code.
Chez BearStudio, on voulait garder le même principe fondamental :
…mais sans éclater le JSX, ni structurer tout le rendu autour des cas.
On voulait pouvoir dire :
« Donne-nous l’état courant, on s’en occupe. Juste assure toi qu’on gère bien tous les états »
Avec ui-state, on transforme la réponse d’un useQuery (ou n’importe quelle source de données) en état unique et explicite, basé sur un seul appel à getUiState.
Voici comment on refactorerait le composant ShoppingList en utilisant ui-state :
import { getUiState } from '@bearstudio/ui-state';
export function ShoppingList() {
const query = useQuery(/* ... */);
const ui = getUiState((set) => {
if(query.status === 'pending') return set('pending');
if(!query.data || query.data.content.length === 0) return set('empty');
return set('default', { data: query.data });
});
return (
<Card>
<CardHeading>
Welcome 👋
{ui
.match(['pending', 'empty'], () => '')
.match('default', ({ data }) => data.title)
.exhaustive()}
</CardHeading>
<CardContent>
{ui
.match('pending', () => <Skeleton />)
.match('empty', () => <EmptyScreen />)
.match('default', ({ data }) => (
<>
{!!data.assignee && <UserInfo {...data.assignee} />}
{data.content.map((item) => (
<ShoppingItem key={item.id} {...item} />
))}
</>
)).exhaustive()}
</CardContent>
</Card>
);
}
Ce qu’on y gagne :
.exhaustive() qui garantit qu’aucun cas n’est oublié.data qui n’est plus typé comme optionnel car on a testé qu’il existait bien.Même principe que dans l’article de TkDodo mais pas besoin de découper en 4 composants ou de structurer l’arbre JSX autour des états.
On garde la logique claire et la composition intacte.
Lien du github : https://github.com/BearStudio/ui-state
Publié le 16/10/2025 dans Développement
Rédigé par :