React - rocambille/start-express-react GitHub Wiki

React est une bibliothèque JavaScript permettant de créer des interfaces utilisateur. Voici un exemple de code pour mesurer si vous êtes à l'aise avec l'outil :

import { createRoot } from "react-dom/client";

function HelloMessage({ who }) {
  return <p>hello, {who}!</p>;
}

const root = createRoot(document.getElementById("container"));
root.render(<HelloMessage who="world" />);

Si vous visualisez le message "hello, world!" affiché dans un conteneur, vous maitrisez les bases de la bibliothèque.

Sinon, nous vous recommandons de revoir les 80% de concepts React avant d'aller plus loin.

Créer des routes

Couplé à React Router en mode data, StartER fournit un point d'entrée pour vos pages React dans le fichier src/react/routes/tsx : c'est dans ce fichier que vous devez déclarer vos routes React. Chaque route comporte deux parties obligatoires : un modèle d’URL et un élément à afficher quand le modèle matche avec l’URL courante dans la barre d'URL du navigateur.

Si vous souhaitez réaliser une SPA, votre fichier routes.tsx devrait ressembler à :

import App from "./components/App";

export default [
  {
    path: "/",
    element: <App />
  },
];

App est le composant racine de votre SPA.

Dans le cas d'une MPA, votre fichier routes.tsx devrait exporter un tableau qui définit plusieurs routes. Un usage courant est d'ajouter un sytème de "layout" pour les éléments communs entre les pages : l'élément <Outlet /> permet l'injection de la partie qui varie d'une page à une autre dans le <Layout>.

function Layout({ children }) {
  return (
    <>
      <Header />
      <main>
        {children}
      </main>
      <Footer />
    </>
  );
}

export default [
  {
    path: "/",
    element: (
      <Layout>
        <Outlet /> {/* <Home /> or <About />,
        depending on the URL in the browser */}
      </Layout>
    ),
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: "/about",
        element: <About />,
      },
    ],
  },
];

Faire un CRUD

StartER propose des exemples de pages et de composants pour manipuler des "items" (un item est ici un objet fictif avec un titre) :

  • <ItemList> affiche une liste d'items,
  • <ItemShow> affiche un item en lecture seule et un formulaire de suppression <ItemDeleteForm>,
  • <ItemEdit> affiche un formulaire <ItemForm> pour modifier un item,
  • <ItemCreate> affiche un formulaire <ItemForm> pour créer un item.

Ces pages et ces composants forment un CRUD pour les items, car ils fournissent les interfaces graphiques pour créer, lire, mettre à jour et supprimer des items.

Chaque composant utilise les données fournies par un Provider qui est lié à un contexte React. Le fichier routes.tsx définit les routes liées au contexte des items.

import { ItemProvider } from "./components/ItemContext";

import ItemCreate from "./pages/ItemCreate";
import ItemEdit from "./pages/ItemEdit";
import ItemList from "./pages/ItemList";
import ItemShow from "./pages/ItemShow";

export default [
  // ...
  {
    path: "/items",
    element: (
      <ItemProvider>
        <Outlet />
      </ItemProvider>
    ),
    children: [
      {
        path: "/items/new",
        element: <ItemCreate />,
      },
      {
        path: "/items/:id/edit",
        element: <ItemEdit />,
      },
      {
        index: true,
        element: <ItemList />,
      },
      {
        path: "/items/:id",
        element: <ItemShow />,
      },
    ],
  },
];

Les pages et composants de ce CRUD se trouvent dans les sous-dossiers pages et components du dossier src/react.

src/react
├── routes.tsx
├── components
|   ├── ItemContext.tsx
|   ├── ItemDeleteForm.tsx
│   └── ItemForm.tsx
└── pages
    ├── ItemCreate.tsx
    ├── ItemEdit.tsx
    ├── ItemList.tsx
    └── ItemShow.tsx

C'est une manière de faire proposée en exemple : vous êtes libres de l'adapter ou d'organiser vos pages/composants d'une manière complètement différente.

Context

Le fichier ItemContext.tsx déclare un contexte React.

import { createContext } from "react";

const ItemContext = createContext(null);

Ce contexte nous permet de construire un provider ItemProvider pour fournir une value au reste de l'application.

import { createContext } from "react";

const ItemContext = createContext(null);

export function ItemProvider({ children }) {
  return (
    <ItemContext value={/* wait for it */}>
      {children}
    </ItemContext>
  );
}

Pour utiliser la valeur, le fichier exporte un hook personnalisé : ce hook est appelé dans les pages et les composants des items.

import { createContext, useContext } from "react";

const ItemContext = createContext(null);

// ...

export const useItems = () => {
  const value = useContext(ItemContext);

  if (value == null) {
    throw new Error("useItems has to be used within <ItemProvider />");
  }

  return value;
};

Reste à remplir la valeur fournie. C'est là que le contexte commence à jouer son rôle : récupérer les données.

import { cache } from "./utils";

// ...

export function ItemProvider({ children }) {
  const items = use(cache("/api/items")) as Item[];

  return (
    <ItemContext value={{ items }}>
      {children}
    </ItemContext>
  );
}

La fonction utilitaire cache permet de fetcher les données avec une mise en cache : c'est nécessaire pour éviter une boucle infinie d'appels avec la fonction use de React.

Également, ItemProvider fournit des fonctions de rappel pour mettre à jour les items : ces fonctions ont la charge d'invalider explicitement le cache.

import { cache, invalidateCache } from "./utils";

// ...

export function ItemProvider({ children }) {
  const items = use(cache("/api/items")) as Item[];

  const addItem = (partialItem: Omit<Item, "id">) => {
    // ...

    invalidateCache("/api/items");
  };

  return (
    <ItemContext value={{ items, addItem }}>
      {children}
    </ItemContext>
  );
}

Voir le code complet pour les détails :

De cette façon, l'essentiel de la logique est centralisé dans ItemContext.tsx. Le reste des pages et composants déroule le minimum d'affichage pour réaliser les fonctionnalités du CRUD et permettre la navigation entre les pages.

List

Objectifs :

  • Afficher la liste des items.
  • Fournir un lien d'accès vers la page Show de chaque item.
  • Fournir un lien d'accès vers la page Create.
import { Link } from "react-router";

import { useItems } from "../components/ItemContext";

function ItemList() {
  const { items } = useItems();

  return (
    <>
      <h1>Items</h1>
      <Link to="/items/new">Ajouter</Link>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            <Link to={`/items/${item.id}`}>{item.title}</Link>
          </li>
        ))}
      </ul>
    </>
  );
}

export default ItemList;

Show

Objectifs :

  • Afficher les détails de l'item courant (le titre).
  • Fournir un lien d'accès vers la page "Edit" pour l'item.
  • Afficher le formulaire de suppression.
import { Link } from "react-router";

import { useItems } from "../components/ItemContext";
import ItemDeleteForm from "../components/ItemDeleteForm";

function ItemShow() {
  const { item } = useItems();

  if (item == null) {
    throw 404;
  }

  return (
    <>
      <h1>{item.title}</h1>
      <Link to={`/items/${item.id}/edit`}>Modifier</Link>
      <ItemDeleteForm />
    </>
  );
}

export default ItemShow;

Note : ItemProvider lit l'id dans l'URL et fournit l'item correspondant via useItems.

Delete

Objectifs :

  • Afficher le formulaire de suppression.
  • Déclencher l'action de suppression à la soumission.
import { useItems } from "./ItemContext";

function ItemDeleteForm() {
  const { deleteItem } = useItems();

  return (
    <form action={deleteItem}>
      <button type="submit">Supprimer</button>
    </form>
  );
}

export default ItemDeleteForm;

Create & Edit

Objectifs pour la page "Create" :

  • Afficher le formulaire.
  • Créer un nouvel item "vide".
import { useItems } from "../components/ItemContext";
import ItemForm from "../components/ItemForm";

function ItemCreate() {
  const { addItem } = useItems();

  const newItem = {
    title: "",
  };

  return (
    <ItemForm defaultValue={newItem} action={addItem}>
      <button type="submit">Ajouter</button>
    </ItemForm>
  );
}

export default ItemCreate;

Objectifs pour la page "Edit" :

  • Afficher le formulaire.
  • Passer l'item existant fourni par ItemProvider.
import { useItems } from "../components/ItemContext";
import ItemForm from "../components/ItemForm";

function ItemEdit() {
  const { item, editItem } = useItems();

  if (item == null) {
    throw 404;
  }

  return (
    <ItemForm defaultValue={item} action={editItem}>
      <button type="submit">Modifier</button>
    </ItemForm>
  );
}

export default ItemEdit;

Le même composant ItemForm est utilisé pour les 2 pages. Les props permettent de :

  • l'alimenter avec un nouvel item ou un item existant,
  • choisir l'action à déclencher à la soumission.
import { type PropsWithChildren, useId } from "react";

interface ItemFormProps extends PropsWithChildren {
  defaultValue: Omit<Item, "id" | "user_id">;
  action: (partialItem: Omit<Item, "id" | "user_id">) => void;
}

function ItemForm({ children, defaultValue, action }: ItemFormProps) {
  const titleId = useId();

  return (
    <form
      action={(formData) => {
        const title = formData.get("title") as string;

        action({ title });
      }}
    >
      <p>
        <label htmlFor={titleId}>title</label>
        <input
          id={titleId}
          type="text"
          name="title"
          defaultValue={defaultValue.title}
        />
      </p>

      {children}
    </form>
  );
}

export default ItemForm;
⚠️ **GitHub.com Fallback** ⚠️