Loaders & Actions

Une route charge ses données avec un loader et gère les mutations avec une action. Les deux sont exportés depuis le fichier de route, aux côtés de la page :

loader:  ({ request, params }) => unknown | Promise<unknown>;
action:  ({ request, params }) => unknown | Promise<unknown>;

Cycle de vie

L'ordre d'exécution dépend de la méthode HTTP :

  1. Sur POST / PUT / PATCH / DELETE, l'action s'exécute en premier.
  2. Le loader s'exécute pour toutes les méthodes — y compris après une action réussie.
  3. Le composant de page se rend avec les résultats.

Les valeurs de retour arrivent dans la page sous forme de props :

Fonction S'exécute sur Retour → prop
loader toutes les méthodes props.data
action méthodes mutantes (POST/PUT/PATCH/DELETE) props.actionData

Accès à la requête et aux sessions

Le composant de page ne voit jamais request — seuls loader et action le voient. Lisez-y les cookies et en-têtes :

import { readSession } from "@/lib/session";

export function loader({ request }) {
  const session = readSession(request); // reads the session cookie off request.headers
  return { user: session?.user ?? null };
}

readSession(request) lit le cookie de session depuis request.headers. Transmettez ce dont la page a besoin via la valeur de retour ; n'essayez pas d'atteindre request depuis le composant.

Suivi des lectures d'en-têtes (vary)

Astuce

Le runtime enregistre quels noms d'en-têtes un loader lit — le « vary set » — et s'en sert pour la mise en cache. Ne lisez que les en-têtes dont vous avez réellement besoin. Lire un en-tête large ou propre à chaque requête (cookies, auth) fait varier la réponse en fonction de celui-ci, ce qui réduit les hits de cache ; ne toucher à rien garde la route cacheable.

Court-circuiter le rendu

Retourner ou lancer une Response depuis l'une ou l'autre fonction court-circuite le rendu de la page (façon Remix). Utilisez cela pour les redirections et les 404 :

import { redirect, notFound } from "@bext-stack/framework";

export function loader({ request, params }) {
  const session = readSession(request);
  if (!session) throw redirect("/login");

  const post = getPost(params.slug);
  if (!post) throw notFound();

  return { post };
}
  • Retournez une Response pour l'envoyer directement.
  • Lancez une Response pour interrompre un traitement plus profond et l'envoyer.

Frontière d'erreur

Les erreurs non rattrapées sont interceptées par l'error.tsx du segment :

/** @jsxImportSource @bext-stack/framework */

export default function Error({ error, reset, route }) {
  return (
    <div role="alert">
      <p>Something went wrong: {String(error)}</p>
      <button onClick="location.reload()">Try again</button>
    </div>
  );
}

reset retente le segment ; route décrit la route en échec.

Exemple complet : page + loader + action

/** @jsxImportSource @bext-stack/framework */
import { redirect } from "@bext-stack/framework";
import { readSession } from "@/lib/session";
import { createPost, listPosts } from "@/lib/posts";

// Runs first on POST, before the loader.
export async function action({ request }) {
  const session = readSession(request);
  if (!session) throw redirect("/login");

  const form = await request.formData();
  const title = String(form.get("title") ?? "");
  if (!title) return { error: "Title is required" };

  await createPost({ authorId: session.user.id, title });
  return { ok: true };
}

// Runs on every method (and after a successful action).
export async function loader({ request }) {
  const session = readSession(request);
  return { posts: await listPosts(), signedIn: Boolean(session) };
}

export default function Page({ data, actionData }) {
  return (
    <main>
      {actionData?.error && <p role="alert">{actionData.error}</p>}
      <form method="post">
        <input name="title" placeholder="Title" />
        <button type="submit">Create</button>
      </form>
      <ul>{data.posts.map((p) => `<li>${p.title}</li>`).join("")}</ul>
    </main>
  );
}

Exemple de route API (route.ts)

Un route.ts gère le HTTP brut avec une fonction par méthode — sans rendu de page :

import { json } from "@bext-stack/framework";
import { listPosts, createPost } from "@/lib/posts";

export async function GET(request: Request) {
  return json(await listPosts());
}

export async function POST(request: Request) {
  const body = await request.json();
  const post = await createPost(body);
  return json(post, { status: 201 });
}

Et ensuite

Page Ce qu'elle couvre
Routage & Rendu Fichiers de route, routes dynamiques, ISR/SSR, helpers de réponse
KV Store Persister un petit état qu'un loader peut lire
Entités (Neon) CRUD piloté par configuration et accès direct à Neon
Sessions & Tokens Comment le cookie / token de session est résolu