EnglishBac à sable

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.