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.

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

Creating a CRUD

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.

Context

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.

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 "../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

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.

Delete

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;

Create & Edit

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;
⚠️ **GitHub.com Fallback** ⚠️