Fermer

janvier 23, 2024

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

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 editUserLoaderon 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.

Restriction du type EditUserLoaderResponse

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 useLoaderDatale 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.

Modifier la fonctionnalité utilisateur

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 deletele deleteUser la fonction est appelée et si c’est savealors editUser est exécuté à la place. C’est ainsi que nous pouvons gérer plusieurs comportements en une seule action.

Supprimer l'utilisateur

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 editUserActionafin 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.

État en attente

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