Skip to content

Commit

Permalink
Add CRUD guide
Browse files Browse the repository at this point in the history
  • Loading branch information
fzaninotto committed Jan 20, 2025
1 parent bb602ad commit 4e0c489
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 11 deletions.
14 changes: 14 additions & 0 deletions docs/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,17 @@ This particular example is sourced from [Atomic CRM](https://marmelab.com/react-
Never hesitate to replace a react-admin component with one of your own design. React-admin does not aim to cover all possible use cases, instead, it provides hooks for incorporating custom components. After all, "It's just React"™.

With react-admin, you'll never find yourself backed into a corner.

## Awesome Developer Experience

With react-admin, developers assemble application components without having to worry about low-level details. They need less code for the same result, and they can **focus on the business logic** of their app.

[![List view without and with react-admin](./img/list-from-react-to-react-admin.webp)](./img/list-from-react-to-react-admin.webp)

We've crafted the API of react-admin's components and hooks to be as **intuitive** as possible. The react-admin core team uses react-admin every day, and we're always looking for ways to improve the developer experience.

React-admin provides the **best-in-class documentation**, demo apps, and support. Error messages are clear and actionable. Thanks to extensive TypeScript types and JSDoc, it's easy to use react-admin in any IDE. The API is stable and **breaking changes are very rare**. You can debug your app with the [query](./DataProviders.md#enabling-query-logs) and [form](https://react-hook-form.com/dev-tools) developer tools, and inspect the react-admin code right in your browser.

That probably explains why more than 3,000 new apps are published every month using react-admin.

So react-admin is not just the assembly of [React Query](https://react-query.tanstack.com/), [react-hook-form](https://react-hook-form.com/), [react-router](https://reacttraining.com/react-router/), [Material UI](https://mui.com/material-ui/getting-started/) and [Emotion](https://github.com/emotion-js/emotion). It's a **framework** made to speed up and facilitate the development of single-page apps in React.
333 changes: 333 additions & 0 deletions docs/CRUD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
---
layout: default
title: "CRUD Pages"
---

# CRUD pages

Most admin and B2B apps start with a few basic screens to manipulate records:

- A list page, including the ability to filter, paginate and sort the records
- A read-only page, displaying the record details
- An edition page, allowing to update the record via a form
- A creation page

We call this type of interface a "CRUD" interface because it allows us to Create, Read, Update and Delete records.

React-admin started as an engine to generate such CRUD interfaces, and it still does it very well. **Building CRUD interfaces with react-admin requires little to no effort**, and it's very easy to customize them.

<video controls autoplay playsinline muted loop width="100%">
<source src="./img/CRUD.webm" type="video/webm" />
<source src="./img/CRUD.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>

## Page Components

React-admin provides page components for CRUD operations:

- [`<List>`](./ListTutorial.md) displays a list of records
- [`<Show>`](./Show.md) displays a record in read-only mode
- [`<Edit>`](./EditTutorial.md) displays a form to edit a record
- [`<Create>`](./Create.md) displays a form to create a record

Each of these components reads the parameters from the URL, fetches the data from the data provider, stores the data in a context, and renders its child component.

For example, to display a list of posts, you would use the `<List>` component:

```jsx
import { List, Datagrid, TextField } from 'react-admin';

const PostList = () => (
<List resource="posts">
<Datagrid>
<TextField source="id" />
<TextField source="title" />
<TextField source="body" />
</Datagrid>
</List>
);
```

Here, the `<List>` component will call `dataProvider.getList('posts')` to fetch the list of posts, and create a `ListContext` to store the data. The `<Datagrid>` component will read the data from that `ListContext` and render a row for each post. That's why there is no need to explicitly pass the data to the `<Datagrid>` component.

## Page Context

`<List>` and other page component don't just fetch data: they provide the way to update the page settings:

- Sort field and order
- Current page & page size
- Filters
- Record selection

The [`ListContext`](./useListContext.md) exposes callbacks to update these settings, and `<List>`'s children components like `<Datagrid>` use these callbacks to update the data.

```jsx
const listContext = useListContext();
const {
// Data
data, // Array of the list records, e.g. [{ id: 123, title: 'hello world' }, { ... }
total, // Total number of results for the current filters, excluding pagination. Useful to build the pagination controls, e.g. 23
meta, // Additional information about the list, like facets & statistics
isPending, // Boolean, true until the data is available
isFetching, // Boolean, true while the data is being fetched, false once the data is fetched
isLoading, // Boolean, true until the data is fetched for the first time
// Pagination
page, // Current page. Starts at 1
perPage, // Number of results per page. Defaults to 25
setPage, // Callback to change the page, e.g. setPage(3)
setPerPage, // Callback to change the number of results per page, e.g. setPerPage(25)
hasPreviousPage, // Boolean, true if the current page is not the first one
hasNextPage, // Boolean, true if the current page is not the last one
// Sorting
sort, // Sort object { field, order }, e.g. { field: 'date', order: 'DESC' }
setSort, // Callback to change the sort, e.g. setSort({ field: 'name', order: 'ASC' })
// Filtering
filterValues, // Dictionary of filter values, e.g. { title: 'lorem', nationality: 'fr' }
displayedFilters, // Dictionary of displayed filters, e.g. { title: true, nationality: true }
setFilters, // Callback to update the filters, e.g. setFilters(filters, displayedFilters)
showFilter, // Callback to show one of the filters, e.g. showFilter('title', defaultValue)
hideFilter, // Callback to hide one of the filters, e.g. hideFilter('title')
// Record selection
selectedIds, // Array listing the ids of the selected records, e.g. [123, 456]
onSelect, // Callback to change the list of selected records, e.g. onSelect([456, 789])
onToggleItem, // Callback to toggle the record selection for a given id, e.g. onToggleItem(456)
onUnselectItems, // Callback to clear the record selection, e.g. onUnselectItems();
// Misc
defaultTitle, // Translated title based on the resource, e.g. 'Posts'
resource, // Resource name, deduced from the location. e.g. 'posts'
refetch, // Callback for fetching the list data again
} = listContext;
```

## The List Page

Children of the `<List>` component display a list of records, and let users change the list parameters.

You can use any of the following components to build the list page:

### List iterators


<table><tbody>
<tr style="border:none">
<td style="width:50%;border:none;text-align:center">
<a title="<Datagrid>" href="./Datagrid.html">
<img src="./img/Datagrid.jpg">
</a>
<a href="./Datagrid.html" style="display: block;transform: translateY(-10px);"><code>&lt;Datagrid&gt;</code></a>
</td>
<td style="width:50%;border:none;text-align:center">
<a title="<DatagridAG>" href="./DatagridAG.html">
<img src="./img/DatagridAG.jpg">
</a>
<a href="./DatagridAG.html" style="display: block;transform: translateY(-10px);"><code>&lt;DatagridAG&gt;</code></a>
</td>
</tr>
<tr style="border:none;background-color:#fff;">
<td style="width:50%;border:none;text-align:center">
<a title="SimpleList" href="./SimpleList.html">
<img src="./img/SimpleList.jpg">
</a>
<a href="./SimpleList.html" style="display: block;transform: translateY(-10px);"><code>&lt;SimpleList&gt;</code></a>
</td>
<td style="width:50%;border:none;text-align:center">
<a title="Calendar" href="./Calendar.html">
<img src="./img/Calendar.jpg">
</a>
<a href="./Calendar.html" style="display: block;transform: translateY(-10px);"><code>&lt;Calendar&gt;</code></a>
</td>
</tr>
</tbody></table>

### Filter components

<table><tbody>
<tr style="border:none">
<td style="width:50%;border:none;text-align:center">
<a title="Filter Button/Form Combo" href="./img/list_filter.mp4">
<video controls autoplay playsinline muted loop>
<source src="./img/list_filter.mp4" type="video/mp4"/>
Your browser does not support the video tag.
</video>
</a>
<a href="./FilteringTutorial.html#the-filter-buttonform-combo" style="display: block;transform: translateY(-10px);">Filter Button/Form Combo</a>
</td>
<td style="width:50%;border:none;text-align:center">
<a title="<FilterList> Sidebar" href="./img/filter-sidebar.webm">
<video controls autoplay playsinline muted loop>
<source src="./img/filter-sidebar.webm" type="video/webm"/>
<source src="./img/filter-sidebar.mp4" type="video/mp4"/>
Your browser does not support the video tag.
</video>
</a>
<a href="./FilteringTutorial.html#the-filterlist-sidebar" style="display: block;transform: translateY(-10px);"><code>&lt;FilterList&gt;</code> Sidebar</a>
</td>
</tr>
<tr style="border:none;background-color:#fff;">
<td style="width:50%;border:none;text-align:center">
<a title="Stacked Filters" href="https://react-admin-ee.marmelab.com/assets/ra-form-layout/latest/stackedfilters-overview.mp4">
<video controls autoplay playsinline muted loop width="90%" style="margin:1rem;box-shadow:0px 4px 4px 0px rgb(0 0 0 / 24%);">
<source src="https://react-admin-ee.marmelab.com/assets/ra-form-layout/latest/stackedfilters-overview.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
</a>
<a href="./FilteringTutorial.html#the-stackedfilters-component" style="display: block;transform: translateY(-10px);"><code>&lt;StackedFilters&gt;</code> Dialog</a>
</td>
<td style="width:50%;border:none;text-align:center;vertical-align:top;">
<a title="<Search> input" href="https://react-admin-ee.marmelab.com/assets/ra-search-overview.mp4"><video controls autoplay playsinline muted loop width="90%" style="margin:1rem;box-shadow:0px 4px 4px 0px rgb(0 0 0 / 24%);">
<source src="https://react-admin-ee.marmelab.com/assets/ra-search-overview.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video></a>
<a href="./FilteringTutorial.html#global-search" style="display: block;transform: translateY(-10px);">Global <code>&lt;Search&gt;</code></a>
</td>
</tr>
</tbody></table>

### Buttons

- [`<EditButton>`](./Buttons.md#editbutton): Go to the edit page for a record
- [`<EditInDialogButton>`](./EditInDialogButton.md): Edit a record in a dialog
- [`<ExportButton>`](./Buttons.md#exportbutton): A button to export the list data
- [`<CreateButton>`](./Buttons.md#createbutton): A button to create a new record
- [`<SortButton>`](./SortButton.md): A button to sort the list
- [`<SelectColumnsButton>`](./SelectColumnsButton.md): A button to select the columns to display in a Datagrid
- [`<BulkUpdateButton>`](./Buttons.md#bulkupdatebutton): A button to update selected records
- [`<BulkDeleteButton>`](./Buttons.md#bulkdeletebutton): A button to delete selected records
- [`<ListActions>`](./ListActions.md): A toolbar with a create and an export button

### Misc

- [`<Pagination>`](./Pagination.md): Renders the page count, and buttons to navigate to the previous and next pages
- [`<SavedQueriesList>`](./SavedQueriesList.md): Lets user save a combination of filters

### Alternatives to List

You can also use specialized alternatives to the `<List>` component, which offer type-specific features:

- [`<TreeWithDetails>`](./TreeWithDetails.md): A tree view with a detail view for each node
- [`<CompleteCalendar>`](./CompleteCalendar.md): A calendar view for events
- [`<InfiniteList>`](./InfiniteList.md): A list with infinite scrolling

## The Show Page

## The Edit & Create Pages

## CRUD Routing

You could declare the CRUD routes manually using react-router's `<Route>` component. But it's such a common pattern that react-admin provides a shortcut: the [`<Resource>`](./Resource.md) component.

```jsx
<Resource
name="posts"
list={PostList} // maps PostList to /posts
show={PostShow} // maps PostShow to /posts/:id/show
edit={PostEdit} // maps PostEdit to /posts/:id
create={PostCreate} // maps PostCreate to /posts/create
/>
```

This is the equivalent to the following react-router configuration:

```jsx
<ResourceContextProvider value="posts">
<Route path="/posts" element={<PostList />} />
<Route path="/posts/:id/show" element={<PostShow />} />
<Route path="/posts/:id" element={<PostEdit />} />
<Route path="/posts/create" element={<PostCreate />} />
</ResourceContextProvider>
```

`<Resource>` defines a `ResourceContext` storing the current resource `name`. This context is used by the `<List>`, `<Edit>`, `<Create>`, and `<Show>` components to determine the resource they should fetch. So when declaring page components with `<Resource>`, you don't need to pass the `resource` prop to them.

```diff
import { List, Datagrid, TextField } from 'react-admin';

const PostList = () => (
- <List resource="posts">
+ <List>
<Datagrid>
<TextField source="id" />
<TextField source="title" />
<TextField source="body" />
</Datagrid>
</List>
);
```

Check [the `<Resource>` documentation](./Resource.md) to learn more about routing and resource context.

## Guessers & Scaffolding

When mapping a new API route to a CRUD view, adding fields one by one can be tedious. React-admin provides a set of guessers that can automatically **generate a complete CRUD UI based on an API response**.

For instance, the following code will generate a complete CRUD UI for the `users` resource:

```jsx
import { Admin, Resource, ListGuesser, EditGuesser, ShowGuesser } from 'react-admin';

const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name="users" list={ListGuesser} edit={EditGuesser} show={ShowGuesser} />
</Admin>
);
```

Guesser components start by fetching data from the API, analyzing the shape of the response, then picking up Field and Input components that match the data type. They also dump the generated code in the console, to let you start customizing the UI.

![ListGuesser](./img/tutorial_guessed_list.png)

Check the following components to learn more about guessers:

- [`<ListGuesser>`](./ListGuesser.md)
- [`<EditGuesser>`](./EditGuesser.md)
- [`<ShowGuesser>`](./ShowGuesser.md)

## Headless Variants

`<List>` and other page components render their children (e.g., `<Datagrid>`) in a page layout. This layout contains a page title (e.g., "Posts"), toolbars for action buttons & filters, a footer for pagination, and a side column.

But sometimes you want to use the list data in a different layout, without the page title and toolbar, or with a different UI kit. For these use cases, you can use the headless variants of the page components, which come in two flavors:

- **Hook**: `useListController`, `useEditController`, `useCreateController`, `useShowController`
- **Component**: `<ListBase>`, `<ShowBase>`, `<EditBase>`, `<CreateBase>`

For instance, to use the list data in a custom layout, you can use the `useListController` hook:

```jsx
import { useListController } from 'react-admin';

const MyList = () => {
const { data, ids, total } = useListController({ resource: 'posts' });
return (
<div>
<h1>Posts</h1>
<ul>
{ids.map(id => (
<li key={id}>{data[id].title}</li>
))}
</ul>
<p>Total: {total}</p>
</div>
);
};
```

If you want to use react-admin components, prefer the Base components, which call the hooks internally and store the values in a context:

```jsx
import { ListBase, Datagrid, TextField } from 'react-admin';

const MyList = () => (
<ListBase resource="posts">
<Datagrid>
<TextField source="id" />
<TextField source="title" />
<TextField source="body" />
</Datagrid>
</ListBase>
);
```



20 changes: 13 additions & 7 deletions docs/DataFetchingGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ You can build a react-admin app on top of any API, whether it uses REST, GraphQL

## The Data Provider

Whenever react-admin needs to communicate with your APIs, it does so through an object called the `dataProvider`. The `dataProvider` exposes a predefined interface that allows react-admin to query any API in a normalized way.
In a react-admin app, you don't write API calls using `fetch` or `axios`. Instead, you communicate with your API through an object called the `dataProvider`.

<img src="./img/data-provider.png" class="no-shadow" alt="Backend agnostic" />

The `dataProvider` exposes a predefined interface that allows react-admin to query any API in a normalized way.

For instance, to query the API for a single record, react-admin calls `dataProvider.getOne()`:

```tsx
Expand All @@ -22,12 +24,6 @@ console.log(response.data); // { id: 123, title: "hello, world" }

The Data Provider is responsible for transforming these method calls into HTTP requests, and converting the responses into the format expected by react-admin. In technical terms, a Data Provider is an *adapter* for an API.

Thanks to this adapter system, react-admin can communicate with any API. Check out the [list of supported backends](./DataProviderList.md) to pick an open-source package for your API.

You can also [write your own Data Provider](./DataProviderWriting.md) to fit your backend's particularities. Data Providers can use `fetch`, `axios`, `apollo-client`, or any other library to communicate with APIs. The Data Provider is also the ideal place to add custom HTTP headers, authentication, etc.

Check out the [Data Provider Setup](./DataProviders.md) documentation for more details on how to set up a Data Provider in your app.

A Data Provider must implement the following methods:

```jsx
Expand All @@ -48,6 +44,16 @@ const dataProvider = {

The Data Provider is a key part of react-admin's architecture. By standardizing the Data Provider interface, react-admin can offer powerful features, like reference handling, optimistic updates, and autogenerated CRUD components.

## Backend Agnostic

Thanks to this adapter system, react-admin can communicate with any API. It doesn't care if your API is a REST API, a GraphQL API, a SOAP API, a JSON-RPC API, or even a local API. It doesn't care if your API is written in PHP, Python, Ruby, Java, or even JavaScript. It doesn't care if your API is a third-party API or a home-grown API.

React-admin ships with [more than 50 data providers](./DataProviderList.md) for popular API flavors.

You can also [write your own Data Provider](./DataProviderWriting.md) to fit your backend's particularities. Data Providers can use `fetch`, `axios`, `apollo-client`, or any other library to communicate with APIs. The Data Provider is also the ideal place to add custom HTTP headers, authentication, etc.

Check out the [Data Provider Setup](./DataProviders.md) documentation for more details on how to set up a Data Provider in your app.

## Calling The Data Provider

Many react-admin components use the Data Provider: page components like `<List>` and `<Edit>`, reference components like `<ReferenceField>` and `<ReferenceInput>`, action Buttons like `<DeleteButton>` and `<SaveButton>`, and many more.
Expand Down
Binary file added docs/img/Calendar.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/Datagrid.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/DatagridAG.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/SimpleList.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 4e0c489

Please sign in to comment.