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.
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 />
},
];
Où 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 />,
},
],
},
];
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.
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.
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;
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
.
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;
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;