React en US - rocambille/start-express-react GitHub Wiki
React is a JavaScript library for building user interfaces. Here's some sample code to gauge your comfort level with the tool:
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" />);If you see the message "hello, world!" displayed in a paragrpah, you've mastered the basics of the library.
Otherwise, we recommend reviewing the 80% of React concepts before proceeding further.
Combined with React Router in data mode, StartER provides an entry point for your React pages in the src/react/routes/tsx file: this is where you must declare your React routes. Each route has two required parts: a URL pattern and an element to display when the pattern matches the current URL in the browser's URL bar.
If you want to create a SPA, your routes.tsx file should look like this:
import App from "./components/App";
export default [
{
path: "/",
element: <App />
},
];Where App is the root component of your SPA.
In the case of a MPA, your routes.tsx file should export an array that defines multiple routes. A common use is to add a layout system for elements common to pages: the <Outlet /> element allows the injection of the part that varies from one page to another into the <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 />,
},
],
},
];Once your routes are defined, you can associate React components with them. StartER provides a complete example around an item resource, illustrating a complete CRUD.
StartER provides example components for manipulating items (an item is a dummy object with a title):
- The
<ItemList>component displays a list of items, - The
<ItemShow>component displays a read-only item and a<ItemDeleteForm>, - The
<ItemEdit>component displays an<ItemForm>for editing an item, - The
<ItemCreate>component displays an<ItemForm>for creating an item.
These components form a CRUD for items, as they provide graphical interfaces for creating, reading, updating, and deleting items.
Each component uses the data and logic provided by a custom hook: in the case of items, this is the useItems hook.
Finally, an index.tsx file defines the routes related to the items.
import type { RouteObject } from "react-router";
import ItemCreate from "./ItemCreate";
import ItemEdit from "./ItemEdit";
import ItemList from "./ItemList";
import ItemShow from "./ItemShow";
export const itemRoutes: RouteObject[] = [
{
path: "/items/new",
element: <ItemCreate />,
},
{
path: "/items/:id/edit",
element: <ItemEdit />,
},
{
path: "/items",
element: <ItemList />,
},
{
path: "/items/:id",
element: <ItemShow />,
},
];The components for this CRUD are located in the item subfolders of the src/react/components folder.
src/react
├── routes.tsx
└── components
└── item
├── hooks.ts
├── index.tsx
├── ItemCreate.tsx
├── ItemDeleteForm.tsx
├── ItemEdit.tsx
├── ItemForm.tsx
├── ItemList.tsx
└── ItemShow.tsx
This is an example of how to do things: you are free to adapt it or organize your components in a completely different way.
💡 To create a new React module from the item module, use the clone script:
npm run make:clone src/react/components/item src/react/components/post Item PostThis script duplicates all module files and automatically replaces the Item identifiers with Post. This gives you a complete React CRUD ready for customization.
A custom hook is a reusable function that encapsulates logic related to a specific need, such as retrieving or updating data.
The hooks.ts file declares a custom hook: useItems.
export const useItems = () => {
// do something
};This custom hook allows us to encapsulate logic and return a representation of it.
export const useItems = () => {
// do some logic
return {
// some values resulting from logic
};
};All that remains is to fill in the value returned by useItems. This is where the custom hook starts to play its role: fetching the data.
import { cache } from "./utils";
export const useItems = () => {
const items = use<Item[]>(cache("/api/items"));
return {
items,
};
};The cache utility function allows you to fetch data with caching: this is necessary to avoid an infinite loop of calls with React's use function.
Also, useItems provides callback functions to update items: these functions are responsible for explicitly invalidating the cache.
import { cache, invalidateCache } from "../utils";
export const useItems = () => {
const items = use<Item[]>(cache("/api/items"));
const addItem = (partialItem: Omit<Item, "id">) => {
// ...
invalidateCache("/api/items");
};
return {
items,
addItem,
};
};See the full code for details:
This way, most of the logic is centralized in useItems. The rest of the components perform the minimum display to perform CRUD functionality and allow navigation between pages.
Objectives:
- Display the list of items.
- Provide a link to each item's Show page.
- Provide a link to the Create page.
import { Link } from "react-router";
import { useItems } from "./hooks";
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;Objectives:
- Display the details of the current item (the title).
- Provide a link to the Edit page for the item.
- Display the deletion form.
import { Link } from "react-router";
import { useItems } from "./hooks";
import ItemDeleteForm from "./ItemDeleteForm";
function ItemShow() {
const { item } = useItems();
if (item == null) {
throw new Error("404");
}
return (
<>
<h1>{item.title}</h1>
<Link to={`/items/${item.id}/edit`}>Modifier</Link>
<ItemDeleteForm />
</>
);
}
export default ItemShow;Note
useItems reads the id in the URL with useParams and provides the corresponding item.
Objectives:
- Display the deletion form.
- Trigger delete action on submission.
import { useItems } from "./hooks";
function ItemDeleteForm() {
const { deleteItem } = useItems();
return (
<form action={deleteItem}>
<button type="submit">Supprimer</button>
</form>
);
}
export default ItemDeleteForm;Objectives for the Create page:
- Display the form.
- Create a new "empty" item.
import { useItems } from "./hooks";
import ItemForm from "./ItemForm";
function ItemCreate() {
const { addItem } = useItems();
const newItem = {
title: "",
};
return (
<ItemForm defaultValue={newItem} action={addItem}>
<button type="submit">Ajouter</button>
</ItemForm>
);
}
export default ItemCreate;Objectives for the Edit page:
- Display the form.
- Pass the existing item provided by
useItems.
import { useItems } from "./hooks";
import ItemForm from "./ItemForm";
function ItemEdit() {
const { item, editItem } = useItems();
if (item == null) {
throw new Error("404");
}
return (
<ItemForm defaultValue={item} action={editItem}>
<button type="submit">Modifier</button>
</ItemForm>
);
}
export default ItemEdit;The same ItemForm component is used for both pages. Props allow you to:
- populate it with a new or existing item,
- choose the action to trigger upon submission.
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")?.toString() ?? "";
// Data should be validated here
action({ title });
}}
>
<p>
<label htmlFor={titleId}>title</label>
<input
id={titleId}
type="text"
name="title"
defaultValue={defaultValue.title}
/>
</p>
{children}
</form>
);
}
export default ItemForm;This pattern makes the code more reusable: a single component handles input and submission, regardless of the page where it is used.