diff --git a/docs/EditTutorial.md b/docs/EditTutorial.md index 446fe4fe205..52414758037 100644 --- a/docs/EditTutorial.md +++ b/docs/EditTutorial.md @@ -13,6 +13,8 @@ React-admin provides many hooks and components to let you build custom user expe Edition views are very common in single-page apps. The most usual way to allow a user to update a record is to fetch the record from an API based on the URL parameters, initialize a form with the record, update the inputs as the user changes the values, and call the API to update the record with the new values upon submission. +[![From Pure React To React-Admin](./img/edit-from-react-to-react-admin.webp)](./img/edit-from-react-to-react-admin.webp) + To better understand how to use the various react-admin hooks and components dedicated to editing and creating, let's start by building such an edition view by hand. ### An Edition View Built By Hand diff --git a/docs/ListTutorial.md b/docs/ListTutorial.md index 0d19614635d..e7d688d77a7 100644 --- a/docs/ListTutorial.md +++ b/docs/ListTutorial.md @@ -11,9 +11,17 @@ The List view displays a list of records, and lets users search for specific rec This tutorial explains the List view from first principles, and shows how react-admin allows you to reduce the amount of boilerplate code to focus on the business logic. -## Building A List View By Hand +## From Pure React To React-Admin -The List view fetches a list of records and renders them, together with UI controls for filter, sort and pagination. You've probably developed it a dozen times, and in fact you don't need react-admin to build, say, a book List view: +The List view fetches a list of records and renders them, together with UI controls for filter, sort and pagination. + +[![From Pure React To React-Admin](./img/list-from-react-to-react-admin.webp)](./img/list-from-react-to-react-admin.webp) + +To better understand how to use the various react-admin hooks and components dedicated to editing and creating, let's start by building such an edition view by hand. + +### A List View Built By Hand + +You've probably developed it a dozen times, and in fact you don't need react-admin to build, say, a book List view: {% raw %} ```jsx @@ -92,30 +100,35 @@ This example uses the `useGetList` hook instead of `fetch` because `useGetList` This list is a bit rough in the edges (for instance, typing in the search input makes one call to the dataProvider per character), but it's good enough for the purpose of this chapter. -## `` Displays Fields In A Table +### `` Displays Fields In A Table Table layouts usually require a lot of code to define the table head, row, columns, etc. React-admin `` component, together with Field components, can help remove that boilerplate: -```jsx +{% raw %} +```diff import { useState } from 'react'; -import { Title, useGetList, Datagrid, TextField } from 'react-admin'; +-import { Title, useGetList } from 'react-admin'; ++import { Title, useGetList, Datagrid, TextField } from 'react-admin'; import { Card, - TextField, + TextField as MuiTextField, Button, Toolbar, +- Table, +- TableHead, +- TableRow, +- TableBody, +- TableCell, } from '@mui/material'; - const BookList = () => { const [filter, setFilter] = useState(''); const [page, setPage] = useState(1); const perPage = 10; - const sort = { field: 'id', order: 'ASC' }; const { data, total, isLoading } = useGetList('books', { filter: { q: filter }, pagination: { page, perPage }, - sort, + sort: { field: 'id', order: 'ASC' } }); if (isLoading) { return
Loading...
; @@ -123,7 +136,7 @@ const BookList = () => { return (
- <TextField + <MuiTextField label="Search" value={filter} onChange={e => setFilter(e.target.value)} @@ -132,12 +145,32 @@ const BookList = () => { margin="dense" /> <Card> - <Datagrid data={data} sort={sort}> - <TextField source="id" /> - <TextField source="title" /> - <TextField source="author" /> - <TextField source="year" /> - </Datagrid> +- <Table sx={{ padding: 2 }} size="small"> +- <TableHead> +- <TableRow> +- <TableCell>Id</TableCell> +- <TableCell>Title</TableCell> +- <TableCell>Author</TableCell> +- <TableCell>Year</TableCell> +- </TableRow> +- </TableHead> +- <TableBody> +- {data.map(book => ( +- <TableRow key={book.id}> +- <TableCell>{book.id}</TableCell> +- <TableCell>{book.title}</TableCell> +- <TableCell>{book.author}</TableCell> +- <TableCell>{book.year}</TableCell> +- </TableRow> +- ))} +- </TableBody> +- </Table> ++ <Datagrid data={data} sort={sort}> ++ <TextField source="id" /> ++ <TextField source="title" /> ++ <TextField source="author" /> ++ <TextField source="year" /> ++ </Datagrid> </Card> <Toolbar> {page > 1 && <Button onClick={() => setPage(page - 1)}>Previous page</Button>} @@ -147,27 +180,29 @@ const BookList = () => { ); }; ``` +{% endraw %} `<Datagrid>` does more than the previous table: it renders table headers depending on the current sort, and allows you to change the sort order by clicking a column header. Also, for each row, `<Datagrid>` creates a `RecordContext`, which lets you use react-admin Field and Buttons without explicitly passing the row data. -## `ListContext` Exposes List Data To Descendants +### `ListContext` Exposes List Data To Descendants `<Datagrid>` requires a `data` prop to render, but it can grab it from a `ListContext` instead. Creating such a context with `<ListContextProvider>` also allows to use other react-admin components specialized in filtering (`<FilterForm>`) and pagination (`<Pagination>`), and to reduce the boilerplate code even further: {% raw %} -```jsx +```diff import { useState } from 'react'; import { Title, useGetList, Datagrid, TextField, - ListContextProvider, - FilterForm, - Pagination, - TextInput ++ ListContextProvider, ++ FilterForm, ++ Pagination, ++ TextInput } from 'react-admin'; -import { Card } from '@mui/material'; +-import { Card, TextField as MuiTextField, Button, Toolbar } from '@mui/material'; ++import { Card } from '@mui/material'; const BookList = () => { const [filter, setFilter] = useState(''); @@ -182,38 +217,53 @@ const BookList = () => { if (isLoading) { return <div>Loading...</div>; } - const filters = [<TextInput label="Search" source="q" size="small" alwaysOn />]; - const filterValues = { q: filter }; - const setFilters = filters => setFilter(filters.q); ++ const filters = [<TextInput label="Search" source="q" size="small" alwaysOn />]; ++ const filterValues = { q: filter }; ++ const setFilters = filters => setFilter(filters.q); return ( - <ListContextProvider value={{ data, total, page, perPage, setPage, filterValues, setFilters, sort }}> - <div> - <Title title="Book list" /> - <FilterForm filters={filters} /> - <Card> - <Datagrid> - <TextField source="id" /> - <TextField source="title" /> - <TextField source="author" /> - <TextField source="year" /> - </Datagrid> - </Card> - <Pagination /> - </div> - </ListContextProvider> ++ <ListContextProvider value={{ data, total, page, perPage, setPage, filterValues, setFilters, sort }}> + <div> + <Title title="Book list" /> +- <MuiTextField +- label="Search" +- value={filter} +- onChange={e => setFilter(e.target.value)} +- variant="filled" +- size="small" +- margin="dense" +- /> ++ <FilterForm filters={filters} /> + <Card> + <Datagrid data={data} sort={sort}> + <TextField source="id" /> + <TextField source="title" /> + <TextField source="author" /> + <TextField source="year" /> + </Datagrid> + </Card> +- <Toolbar> +- {page > 1 && <Button onClick={() => setPage(page - 1)}>Previous page</Button>} +- {page < total / perPage && <Button onClick={() => setPage(page + 1)}>Next page</Button>} +- </Toolbar> ++ <Pagination /> + </div> ++ </ListContextProvider> ); }; ``` {% endraw %} -## `useListController` Handles Controller Logic +### `useListController` Handles Controller Logic The initial logic that grabs the records from the API, handles the filter and pagination state, and creates callbacks to change them is also common, and react-admin exposes [the `useListController` hook](./useListController.md) to do it. It returns an object that fits perfectly the format expected by `<ListContextProvider>`: -```jsx +{% raw %} +```diff +-import { useState } from 'react'; import { Title, - useListController, +- useGetList, ++ useListController, Datagrid, TextField, ListContextProvider, @@ -224,13 +274,28 @@ import { import { Card } from '@mui/material'; const BookList = () => { - const listContext = useListController(); - if (listContext.isLoading) { - return <div>Loading...</div>; - } +- const [filter, setFilter] = useState(''); +- const [page, setPage] = useState(1); +- const perPage = 10; +- const sort = { field: 'id', order: 'ASC' }; +- const { data, total, isLoading } = useGetList('books', { +- filter: { q: filter }, +- pagination: { page, perPage }, +- sort, +- }); +- if (isLoading) { +- return <div>Loading...</div>; +- } ++ const listContext = useListController(); ++ if (listContext.isLoading) { ++ return <div>Loading...</div>; ++ } const filters = [<TextInput label="Search" source="q" size="small" alwaysOn />]; +- const filterValues = { q: filter }; +- const setFilters = filters => setFilter(filters.q); return ( - <ListContextProvider value={listContext}> +- <ListContextProvider value={{ data, total, page, perPage, setPage, filterValues, setFilters, sort }}> ++ <ListContextProvider value={listContext}> <div> <Title title="Book list" /> <FilterForm filters={filters} /> @@ -248,6 +313,7 @@ const BookList = () => { ); }; ``` +{% endraw %} Notice that `useListController` doesn't need the 'books' resource name - it relies on the `ResourceContext`, set by the `<Resource>` component, to guess it. @@ -262,46 +328,57 @@ React-admin's List controller does much, much more than the code it replaces abo - it changes the current page if it's empty, - it translates the title -## `<ListBase>`: Component Version Of The Controller +### `<ListBase>`: Component Version Of The Controller As calling the List controller and putting its result into a context is also common, react-admin provides [the `<ListBase>` component](./ListBase.md) to do it. So the example can be further simplified to the following: -```jsx +```diff import { Title, - ListBase, +- useListController, Datagrid, TextField, +- ListContextProvider, ++ ListBase, FilterForm, Pagination, TextInput } from 'react-admin'; import { Card } from '@mui/material'; -const filters = [<TextInput label="Search" source="q" size="small" alwaysOn />]; ++const filters = [<TextInput label="Search" source="q" size="small" alwaysOn />]; -const BookList = () => ( - <ListBase> - <div> - <Title title="Book list" /> - <FilterForm filters={filters} /> - <Card> - <Datagrid> - <TextField source="id" /> - <TextField source="title" /> - <TextField source="author" /> - <TextField source="year" /> - </Datagrid> - </Card> - <Pagination /> - </div> - </ListBase> -); +const BookList = () => { +- const listContext = useListController(); +- if (listContext.isLoading) { +- return <div>Loading...</div>; +- } +- const filters = [<TextInput label="Search" source="q" size="small" alwaysOn />]; + return ( +- <ListContextProvider value={listContext}> ++ <ListBase> + <div> + <Title title="Book list" /> + <FilterForm filters={filters} /> + <Card> + <Datagrid> + <TextField source="id" /> + <TextField source="title" /> + <TextField source="author" /> + <TextField source="year" /> + </Datagrid> + </Card> + <Pagination /> + </div> +- </ListContextProvider> ++ </ListBase> + ); +}; ``` Notice that we're not handling the loading state manually anymore. In fact, the `<Datagrid>` component can render a skeleton while the data is being fetched. -## `useListContext` Accesses The List Context +### `useListContext` Accesses The List Context Using the `<ListBase>` component has one drawback: you can no longer access the list context (`data`, `total`, etc.) in the component. Instead, you have to access it from the `ListContext` using [the `useListContext` hook](./useListContext.md). @@ -322,10 +399,50 @@ const Pagination = () => { } ``` -## `<List>` Renders Title, Filters, And Pagination +### `<List>` Renders Title, Filters, And Pagination `<ListBase>` is a headless component: it renders only its children. But almost every List view needs a wrapping `<div>`, a title, filters, pagination, a MUI `<Card>`, etc. That's why react-admin provides [the `<List>` component](./List.md), which includes the `<ListBase>` component and a "classic" layout to reduce the boilerplate even further: +```diff +import { +- Title, +- ListBase, ++ List, + Datagrid, + TextField, +- FilterForm, +- Pagination, + TextInput +} from 'react-admin'; +-import { Card } from '@mui/material'; + +const filters = [<TextInput label="Search" source="q" size="small" alwaysOn />]; + +const BookList = () => ( +- <ListBase> +- <div> +- <Title title="Book list" /> +- <FilterForm filters={filters} /> +- <Card> ++ <List filters={filters}> + <Datagrid> + <TextField source="id" /> + <TextField source="title" /> + <TextField source="author" /> + <TextField source="year" /> + </Datagrid> +- </Card> +- <Pagination /> +- </div> +- </ListBase> ++ </List> +); +``` + +## A Typical React-Admin List View + +Remember the first snippet in this page? The react-admin version is much shorter, and more expressive: + ```jsx import { List, @@ -349,7 +466,7 @@ const BookList = () => ( ); ``` -Now compare this code snippet with the first snippet in this page: it's much shorter, and more expressive! By encapsulating common CRUD logic, react-admin reduces the amount of code you need to write, and lets you focus on the business logic. As you've seen with the List controller and context, there is no magic: it's just standard React hooks and components designed for B2B apps and web developers with deadlines. +By encapsulating common CRUD logic, react-admin reduces the amount of code you need to write, and lets you focus on the business logic. As you've seen with the List controller and context, there is no magic: it's just standard React hooks and components designed for B2B apps and web developers with deadlines. ## `<ListGuesser>`: Zero-Configuration List diff --git a/docs/img/edit-from-react-to-react-admin.webp b/docs/img/edit-from-react-to-react-admin.webp new file mode 100644 index 00000000000..a241ded7078 Binary files /dev/null and b/docs/img/edit-from-react-to-react-admin.webp differ diff --git a/docs/img/list-from-react-to-react-admin.webp b/docs/img/list-from-react-to-react-admin.webp new file mode 100644 index 00000000000..fa1be0b09c0 Binary files /dev/null and b/docs/img/list-from-react-to-react-admin.webp differ