Démystifier les chargeurs et les actions dans React Router 6 (partie 2)

React Router 6 a introduit un large éventail de nouvelles fonctionnalités qui ont révolutionné la récupération et la soumission de données dans les applications React. Découvrez comment utiliser les chargeurs et les actions pour créer, mettre à jour, supprimer et lire les données des utilisateurs dans cette série en deux parties.
Dans le première partie de cette série, nous avons expliqué comment utiliser les chargeurs de routeur React pour implémenter la récupération de données et les actions pour gérer la soumission des données du formulaire et leur envoi via une requête API. Nous allons maintenant implémenter la fonctionnalité de modification et de suppression et explorer comment ajouter des commentaires sur l’état en attente de l’interface utilisateur afin que les utilisateurs sachent que le formulaire est en cours de traitement.
Modification d’un utilisateur
Commençons par ajouter un nouvel itinéraire dans le main.tsx
déposer.
src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Users from "./views/user/Users.tsx";
import { usersLoader } from "./views/user/Users.loader.ts";
import EditUser from "./views/user/EditUser.tsx";
import { editUserLoader } from "./views/user/EditUser.loader.ts";
import { editUserAction } from "./views/user/EditUser.action.ts";
import CreateUser from "./views/user/CreateUser.tsx";
import { createUserAction } from "./views/user/CreateUser.action.ts";
const router = createBrowserRouter([
{
path: "https://www.telerik.com/",
element: <App />,
children: [
{
index: true,
element: <Users />,
loader: usersLoader,
},
{
path: "/user/create",
element: <CreateUser />,
action: createUserAction,
},
{
path: "/user/:id",
element: <EditUser />,
loader: editUserLoader,
action: editUserAction,
},
],
},
]);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
Le EditComponent
sera chargé lorsque l’URL correspond /user/:id
chemin. Le :id
param est dynamique, il correspondra donc à tout, à l’exception de create
, qui est déjà utilisé pour la route d’ajout d’utilisateur. Contrairement aux autres routes, la nouvelle route d’édition a défini à la fois un chargeur et une action car nous devons récupérer les données de l’utilisateur correspondant et nous avons besoin d’une action pour soumettre les données. Ajoutons-les ensuite.
src/views/user/EditUser.loader.ts
import { LoaderFunctionArgs, redirect } from "react-router-dom";
import { z } from "zod";
import { userSchema } from "../../schema/user.schema";
export const editUserLoader = async ({ params }: LoaderFunctionArgs) => {
try {
const response = await fetch(`http://localhost:4000/users/${params.id}`);
const user = await response.json();
return {
user: userSchema.parse(user),
};
} catch (error) {
return redirect("https://www.telerik.com/");
}
};
export type EditUserLoaderResponse = Exclude<
Awaited<ReturnType<typeof editUserLoader>>,
Response
>;
Dans le editUserLoader
on prend le id
paramètre et utilisez-le pour récupérer des détails sur l’utilisateur. Lorsque nous avons la réponse, nous validons les données utilisateur reçues. En cas de problème, l’utilisateur est redirigé vers la page des utilisateurs. Notez comment nous excluons le Response
interface depuis le EditUserLoaderResponse
taper. La raison en est que nous voulons utiliser EditUserLoaderResponse
pour affirmer le type de données renvoyées par le chargeur à l’intérieur d’un composant. Cependant, même si l’objet est revenu dans le try
le bloc contient le user
propriété, le redirect
méthode renvoie le Response
taper. Par conséquent, le type de retour attendu du editUserLoader
c’est quelque chose comme ça :
type EditUserLoaderResponse = {
user: { id: string | number; firstName: string; lastName: string; }
} | Response
L’image ci-dessous montre l’erreur qui se produirait avec le type ci-dessus.
On peut exclure le Response
tapez car nous savons que si le chargeur renvoie un redirect
, le composant de route correspondant à l’URL actuelle ne sera pas rendu du tout. Par conséquent, il est raisonnable de supposer que ce qui est renvoyé par le useLoaderData
le crochet sera l’objet avec le user
propriété. Ajoutons ensuite l’action d’itinéraire.
src/views/user/EditUser.action.ts
import { ActionFunctionArgs, redirect } from "react-router-dom";
export const editUserAction = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const payload = Object.fromEntries(formData.entries());
await fetch(`http://localhost:4000/users/${payload.id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
return redirect("https://www.telerik.com/");
};
Le editUserAction
est très similaire à createUserAction
fonction. La seule différence est que nous devons utiliser l’ID de l’utilisateur dans l’URL. Lorsque la demande aboutit, l’utilisateur est redirigé vers la page des utilisateurs.
Les chargeurs et les actions permettant d’éditer un utilisateur sont prêts, il est donc temps de créer le EditUser
composant.
src/views/user/EditUser.tsx
import { useLoaderData } from "react-router-dom";
import { EditUserLoaderResponse } from "./EditUser.loader";
import UserForm from "./components/UserForm";
const EditUser = () => {
const { user } = useLoaderData() as EditUserLoaderResponse;
return (
<div className="max-w-sm mx-auto">
<UserForm user={user} action={`/user/${user.id}`} />
</div>
);
};
export default EditUser;
Nous récupérons le récupéré user
données du chargeur et les transmettre au UserForm
composant. En plus de cela, nous rendons également le UserForm
composant et réussite user
et action
accessoires. Nous n’avons pas besoin de créer le UserForm
composant, comme nous l’avons déjà fait dans la partie précédente de cette série, mais voici le code pour rappel.
src/views/user/components/UserForm.tsx
import { Form } from "react-router-dom";
type UserFormProps = {
className?: string;
user?: {
id: string | number;
firstName: string;
lastName: string;
} | null;
action: string;
};
const UserForm = (props: UserFormProps) => {
const { className, user, action } = props;
return (
<div className={className}>
<Form className="space-y-4" method="post" action={action}>
<input type="hidden" name="id" defaultValue={user?.id} />
<div className="flex flex-col items-start gap-y-2">
<label>First Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="firstName"
defaultValue={user?.firstName || ""}
/>
</div>
<div className="flex flex-col items-start gap-y-2">
<label>Last Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="lastName"
defaultValue={user?.lastName || ""}
/>
</div>
<div>
<button
type="submit"
className="w-full px-4 py-3 mt-4 font-semibold bg-sky-600 text-sky-50"
>
Save
</button>
</div>
</Form>
</div>
);
};
export default UserForm;
C’est tout le code dont nous avons besoin pour la fonctionnalité d’édition. Nous pouvons cliquer sur l’un des utilisateurs, mettre à jour le nom et le prénom et cliquer sur le Save
bouton pour mettre à jour l’utilisateur. Le GIF ci-dessous montre à quoi cela ressemble.
Nous pouvons créer, modifier et afficher des utilisateurs, mais nous ne pouvons pas encore les supprimer, ajoutons donc cette fonctionnalité.
Supprimer un utilisateur : comment gérer plusieurs actions ?
Nous ajouterons un bouton Supprimer dans le UserForm
composant, afin que nous puissions mettre à jour ou supprimer un utilisateur. Cependant, avant de faire cela, nous devons répondre à une question importante : comment pouvons-nous avoir plusieurs actions par itinéraire ? Après tout, nous avons besoin d’une action pour mettre à jour un utilisateur et d’une autre pour le supprimer. Le problème, c’est que nous ne pouvons pas. Nous ne pouvons avoir qu’une seule action.
Cependant, au sein d’une action, nous pouvons déterminer ce qui doit être fait en fonction de la charge utile. Par conséquent, nous ajouterons un bouton de suppression et mettrons à jour le bouton de sauvegarde actuel avec deux attributs : name
et value
. Ceux-ci seront utilisés pour exécuter le flux de mise à jour ou de suppression.
src/views/user/components/UserForm.tsx
import { Form } from "react-router-dom";
type UserFormProps = {
className?: string;
user?: {
id: string | number;
firstName: string;
lastName: string;
} | null;
action: string;
};
const UserForm = (props: UserFormProps) => {
const { className, user, action } = props;
return (
<div className={className}>
<Form className="space-y-4" method="post" action={action}>
<input type="hidden" name="id" defaultValue={user?.id} />
<div className="flex flex-col items-start gap-y-2">
<label>First Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="firstName"
defaultValue={user?.firstName || ""}
/>
</div>
<div className="flex flex-col items-start gap-y-2">
<label>Last Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="lastName"
defaultValue={user?.lastName || ""}
/>
</div>
<div className="space-y-4">
<button
type="submit"
name="intent"
value="save
className="w-full px-4 py-3 mt-4 font-semibold bg-sky-600 text-sky-50"
>
Save
</button>
{user ? (
<button
type="submit"
name="intent"
value="delete"
className="w-full px-4 py-3 font-semibold bg-gray-100 hover:bg-gray-200"
>
Delete
</button>
) : null}
</div>
</Form>
</div>
);
};
export default UserForm;
Ensuite, nous devons mettre à jour le editUserAction
méthode.
src/views/user/EditUser.action.ts
import { ActionFunctionArgs, redirect } from "react-router-dom";
import { User, userSchema } from "../../schema/user.schema";
const deleteUser = async (userId: string | number) => {
return fetch(`http://localhost:4000/users/${userId}`, {
method: "delete",
});
};
const editUser = async (payload: User) => {
return fetch(`http://localhost:4000/users/${payload.id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
};
export const editUserAction = async (args: ActionFunctionArgs) => {
const { request } = args;
const formData = await request.formData();
const { intent, ...payload } = Object.fromEntries(formData.entries());
const userData = userSchema.parse(payload);
if (intent === "delete") {
await deleteUser(userData.id);
}
if (intent === "save") {
await editUser(userData);
}
return redirect("https://www.telerik.com/");
};
À l’intérieur de editUserAction
fonction, le intent
la valeur est séparée du reste des données du formulaire. Si sa valeur est delete
le deleteUser
la fonction est appelée et si c’est save
alors editUser
est exécuté à la place. C’est ainsi que nous pouvons gérer plusieurs comportements en une seule action.
Comment afficher un état en attente lors de la soumission du formulaire ?
Parfois, le traitement des requêtes API peut prendre un certain temps. Par conséquent, pour améliorer l’expérience utilisateur, nous pouvons afficher un retour indiquant que quelque chose se passe en réponse à l’interaction de l’utilisateur.
Par exemple, si un utilisateur clique sur les boutons Enregistrer ou Supprimer, nous pourrions modifier le texte ou afficher une double flèche. Pour simplifier les choses, nous allons simplement modifier les textes de Save
à Saving...
et Delete
à Deleting...
. Les boutons seront également désactivés lorsque l’action est en attente.
React Router 6 a un hook appelé useNavigation
qui fournit des informations sur la navigation dans les pages. Il peut être utilisé pour obtenir des informations, par exemple s’il y a une navigation en attente et plus encore. Tu peux en savoir plus à ce sujet en détail ici. Nous utiliserons ce crochet dans le UserForm
composant pour désactiver les boutons du formulaire et modifier leur texte.
src/views/user/components/UserForm.tsx
import { Form, useNavigation } from "react-router-dom";
type UserFormProps = {
className?: string;
user?: {
id: string | number;
firstName: string;
lastName: string;
} | null;
action: string;
};
const UserForm = (props: UserFormProps) => {
const { className, user, action } = props;
const navigation = useNavigation();
const isSubmitPending =
navigation.state === "submitting" && navigation.formMethod === "post";
const isDeletePending =
navigation.state === "submitting" && navigation.formMethod === "delete";
return (
<div className={className}>
<Form className="space-y-4" method="post" action={action}>
<input type="hidden" name="id" defaultValue={user?.id} />
<div className="flex flex-col items-start gap-y-2">
<label>First Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="firstName"
defaultValue={user?.firstName || ""}
/>
</div>
<div className="flex flex-col items-start gap-y-2">
<label>Last Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="lastName"
defaultValue={user?.lastName || ""}
/>
</div>
<div className="space-y-4">
<button
type="submit"
name="intent"
value="save"
className="w-full px-4 py-3 mt-4 font-semibold bg-sky-600 text-sky-50"
disabled={isSubmitPending || isDeletePending}
>
{isSubmitPending ? "Saving..." : "Save"}
</button>
{user ? (
<button
type="submit"
name="intent"
value="delete"
formMethod="delete"
className="w-full px-4 py-3 font-semibold bg-gray-100 hover:bg-gray-200"
disabled={isSubmitPending || isDeletePending}
>
{isDeletePending ? "Deleting..." : "Delete"}
</button>
) : null}
</div>
</Form>
</div>
);
};
export default UserForm;
Nous utilisons navigation.state
et navigation.formMethod
obtenir isSubmitPending
et isDeletePending
valeurs.
const navigation = useNavigation();
const isSubmitPending =
navigation.state === "submitting" && navigation.formMethod === "post";
const isDeletePending =
navigation.state === "submitting" && navigation.formMethod === "delete";
Ceux-ci sont ensuite utilisés dans les boutons Enregistrer et Supprimer.
<button
type="submit"
name="intent"
value="save"
formMethod="post"
className="w-full px-4 py-3 mt-4 font-semibold bg-sky-600 text-sky-50"
disabled={isSubmitPending || isDeletePending}
>
{isSubmitPending ? "Saving..." : "Save"}
</button>
{user ? (
<button
type="submit"
name="intent"
value="delete"
formMethod="delete"
className="w-full px-4 py-3 font-semibold bg-gray-100 hover:bg-gray-200"
disabled={isSubmitPending || isDeletePending}
>
{isDeletePending ? "Deleting..." : "Delete"}
</button>
) : null}
Notez que le Delete
Le bouton a également un nouvel attribut appelé formMethod
. Lorsque le formulaire est soumis à l’aide du Delete
bouton, sa méthode de formulaire passe de post
à delete
. Cet attribut est nécessaire pour que nous puissions distinguer les boutons d’enregistrement et de suppression et afficher un texte différent pour le bouton sur lequel vous avez cliqué. Avant de terminer, ajoutons un peu de retard artificiel au editUserAction
afin que nous puissions voir la mise à jour du texte.
src/views/user/EditUser.action.ts
export const editUserAction = async (args: ActionFunctionArgs) => {
await new Promise(resolve => setTimeout(resolve, 1000));
return redirect("https://www.telerik.com/");
};
Voici à quoi cela ressemble en action.
Conclusion
Les chargeurs et les actions sont des fonctionnalités puissantes qui résolvent les problèmes courants de récupération et de soumission de données dans les applications React. Ce sont d’excellents outils qui peuvent améliorer les fonctionnalités globales et l’expérience utilisateur.
En utilisant des chargeurs, nous surmontons le problème de cascade causé par la récupération de données dans les composants et dissocions les composants de la récupération de données.
Les actions, quant à elles, offrent une approche simple pour gérer les soumissions de formulaires et effectuer les actions nécessaires avant de naviguer.
Nous n’avons couvert qu’une partie des nouvelles fonctionnalités introduites dans Réagir au routeur 6alors assurez-vous de consulter la documentation pour en savoir plus.
Source link