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 container, 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 />,
},
],
},
];
StartER provides sample pages and components for manipulating "items" (an item is a dummy object with a title):
-
<ItemList>
displays a list of items, -
<ItemShow>
displays a read-only item and a<ItemDeleteForm>
delete form, -
<ItemEdit>
displays a<ItemForm>
form for editing an item, -
<ItemCreate>
displays a<ItemForm>
form for creating an item.
These pages and components form a CRUD for items, as they provide graphical interfaces for creating, reading, updating, and deleting items.
Each component uses data provided by a Provider
which is bound to a React context. The routes.tsx
file defines the routes related to the items' context.
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 />,
},
],
},
];
The pages and components for this CRUD are located in the pages
and components
subfolders of the src/react
folder.
src/react
├── routes.tsx
├── components
| ├── ItemContext.tsx
| ├── ItemDeleteForm.tsx
│ └── ItemForm.tsx
└── pages
├── ItemCreate.tsx
├── ItemEdit.tsx
├── ItemList.tsx
└── ItemShow.tsx
This is an example of how to do things: you are free to adapt it or organize your pages/components in a completely different way.
The ItemContext.tsx
file declares a React context.
import { createContext } from "react";
const ItemContext = createContext(null);
This context allows us to build an ItemProvider
to provide a value to the rest of the application.
import { createContext } from "react";
const ItemContext = createContext(null);
export function ItemProvider({ children }) {
return (
<ItemContext value={/* wait for it */}>
{children}
</ItemContext>
);
}
To use the value, the file exports a custom hook: this hook is called in the pages and components of the 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;
};
All that's left is to fill in the provided value. This is where context begins to play its role: retrieving the data.
import { cache } from "./utils";
// ...
export function ItemProvider({ children }) {
const items = use(cache("/api/items")) as Item[];
return (
<ItemContext value={{ items }}>
{children}
</ItemContext>
);
}
The cache
utility function allows you to fetch data with caching: this is necessary to avoid an infinite render loop in React's use
function.
Also, ItemProvider
provides callback functions to update items: these functions are responsible for explicitly invalidating the 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>
);
}
See the full code for details:
This way, most of the logic is centralized in ItemContext.tsx
. The rest of the pages and components perform the minimum display to achieve 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 "../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;
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 "../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
reads the id in the URL and provides the corresponding item via useItems
.
Objectives:
- Display the deletion form.
- Trigger the deletion action upon submission.
import { useItems } from "./ItemContext";
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 "../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;
Objectives for the Edit page:
- Display the form.
- Pass the existing item provided by
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;
The same ItemForm
component is used for both pages. The 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") 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;