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.

Creating routes

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.

Creating a 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 Post

This script duplicates all module files and automatically replaces the Item identifiers with Post. This gives you a complete React CRUD ready for customization.

Custom hook

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.

List

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;

Show

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.

Delete

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;

Create & Edit

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.

⚠️ **GitHub.com Fallback** ⚠️