Routage
@fluixi/start fournit un routage par fichiers : les fichiers de src/routes/
deviennent des routes.
src/routes/
index.tsx → /
about.tsx → /about
users/
layout.tsx → englobe tout ce qui est sous /users
index.tsx → /users
[id].tsx → /users/:id
blog/
[...slug].tsx → /blog/* (attrape-tout)
(auth)/
login.tsx → /login — le groupe n'apparaît pas dans l'URL
Quatre conventions, et c'est toute la couche fichier :
| Fichier | Signification |
|---|---|
index.tsx |
le chemin du dossier lui-même |
[id].tsx |
un segment dynamique, exposé comme :id |
[...slug].tsx |
attrape-tout — correspond au reste du chemin |
layout.tsx |
englobe les routes voisines et situées en dessous |
(nom)/ |
regroupement seul — organise les fichiers sans ajouter de segment d'URL |
Un groupe (auth) est aplati : (auth)/login.tsx sert /login, pas /auth/login. Il
existe pour qu'un ensemble de routes partage un dossier — et, si vous en ajoutez un, un
layout.tsx — sans que ce dossier apparaisse dans l'URL.
Segments dynamiques et paramètres
[param] capture un segment. useParams renvoie un accesseur, car naviguer entre deux
correspondances de la même route change les paramètres sans démonter le composant :
import { useParams } from '@fluixi/start/router';
export default function Post() {
const params = useParams<{ slug: string }>();
return <h1>{params().slug}</h1>;
}
Des hooks apparentés, pour des besoins plus étroits :
const id = useParam('id'); // un paramètre, en accesseur
const own = useLevelParams(); // seulement ceux introduits à ce niveau
const matched = useMatch(() => '/users/:id'); // ce motif correspond-il actuellement ?
Layouts imbriqués
Un layout.tsx englobe ses routes enfants autour d'un <Outlet/> :
import { Outlet } from '@fluixi/start/router';
export default function UsersLayout() {
return (
<div class="users">
<Sidebar />
<Outlet />
</div>
);
}
Un layout reste monté pendant la navigation entre les routes qu'il contient : l'état qu'il détient — une position de défilement, un panneau ouvert, une liste chargée — survit à la transition. C'est la raison principale d'en utiliser un.
Navigation
<Link> navigue côté client ; un <a href> simple recharge la page entière.
import { Link } from '@fluixi/start/router';
<Link href="/about">À propos</Link>
<Link href="/users/42" replace>Remplacer l'entrée d'historique</Link>
<Link href="/docs" activeClass="on">Docs</Link>
activeClass s'applique tant que le lien correspond au chemin courant : une mise en
évidence de navigation ne demande aucun câblage supplémentaire. active prend le pas sur
cette décision quand il vous faut autre chose qu'une correspondance exacte. A est un
alias de Link.
Pour naviguer par programme :
import { useNavigate } from '@fluixi/start/router';
const navigate = useNavigate();
navigate('/dashboard');
navigate('/login', { replace: true });
Emplacement et chaîne de requête
useLocation() renvoie un objet vivant — lisez les propriétés directement, sans appel :
const location = useLocation();
location.pathname; // réactif
useSearchParams est une paire lecture/écriture. Mettre une clé à null la supprime :
const [params, setParams] = useSearchParams();
params().page; // « 2 »
setParams({ page: '3' }); // ?page=3
setParams({ page: null }); // la supprime
setParams({ page: '1' }, { replace: true });
Charger des données
Un fichier de route peut exporter routeData ; le plus proche est accessible via
useRouteData :
export function routeData({ params }) {
return getUser(params().id); // params est un accesseur, pas un objet simple
}
export default function User() {
const user = useRouteData<User>();
return <h1>{user()?.name}</h1>;
}
useRouteData renvoie un accesseur qui suspend sous <Suspense> pendant le chargement :
une frontière au-dessus affiche donc son fallback plutôt qu'un état vide clignotant.
Pour des données non liées à une route, createAsync transforme un fetcher en ressource :
import { createAsync, cache } from '@fluixi/start/router';
const getUser = cache((id: string) => fetchUser(id), 'user');
const user = createAsync(() => getUser(props.id));
cache donne à la fonction une clé stable : les appels identiques sont dédupliqués, les
résultats sont transférés du serveur au client à l'hydratation au lieu d'être récupérés
deux fois, et revalidate('user') peut les invalider.
Mutations
action enveloppe une mutation pour rendre sa progression observable :
import { action, useSubmission, Form } from '@fluixi/start/router';
const save = action(async (data: FormData) => {
await updateProfile(data);
}, 'save-profile');
function Profile() {
const submission = useSubmission(save);
return (
<Form action={save}>
<input name="name" />
<button disabled={!!submission()?.pending}>Enregistrer</button>
</Form>
);
}
Utilisez <Form>, pas un <form action={save}> simple : l'action est une fonction, et un
attribut action doit être une URL. <Form> construit le FormData, appelle l'action sans
navigation, et émet quand même method="post" avec la véritable URL de l'action dans le
balisage — l'envoi fonctionne donc avant le chargement de JavaScript.
useSubmission rapporte l'état en cours — en attente, résultat, erreur — avec retry et
clear : exactement ce qu'il faut à un formulaire pour désactiver son bouton et signaler un
échec sans suivre cela à la main.
Ensuite : Fonctions serveur.