diff --git a/.prettierignore b/.prettierignore index 4d29bce66b3..914b318669d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -23,4 +23,5 @@ cypress/screenshots !.yarn/plugins !.yarn/releases !.yarn/sdks -!.yarn/versions \ No newline at end of file +!.yarn/versions +.astro \ No newline at end of file diff --git a/Makefile b/Makefile index 910cb810887..e380f98f71e 100644 --- a/Makefile +++ b/Makefile @@ -117,6 +117,9 @@ build: build-ra-core build-ra-data-fakerest build-ra-ui-materialui build-ra-data doc: ## compile doc as html and launch doc web server @yarn doc +doc-headless: ## launch headless doc web server + @cd docs_headless && yarn dev + docker-doc: ## run the doc website in a docker container @docker run -it --rm \ -p 4000:4000 \ diff --git a/docs_headless/.gitignore b/docs_headless/.gitignore new file mode 100644 index 00000000000..6240da8b10b --- /dev/null +++ b/docs_headless/.gitignore @@ -0,0 +1,21 @@ +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store diff --git a/docs_headless/README.md b/docs_headless/README.md new file mode 100644 index 00000000000..fcd63ad7b36 --- /dev/null +++ b/docs_headless/README.md @@ -0,0 +1,54 @@ +# Starlight Starter Kit: Basics + +[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) + +``` +npm create astro@latest -- --template starlight +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) +[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) +[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs) + +> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! + +## πŸš€ Project Structure + +Inside of your Astro + Starlight project, you'll see the following folders and files: + +``` +. +β”œβ”€β”€ public/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ assets/ +β”‚ β”œβ”€β”€ content/ +β”‚ β”‚ └── docs/ +β”‚ └── content.config.ts +β”œβ”€β”€ astro.config.mjs +β”œβ”€β”€ package.json +└── tsconfig.json +``` + +Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. + +Images can be added to `src/assets/` and embedded in Markdown with a relative link. + +Static assets, like favicons, can be placed in the `public/` directory. + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | +| `npm run astro -- --help` | Get help using the Astro CLI | + +## πŸ‘€ Want to learn more? + +Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). diff --git a/docs_headless/astro.config.mjs b/docs_headless/astro.config.mjs new file mode 100644 index 00000000000..dd3df21e817 --- /dev/null +++ b/docs_headless/astro.config.mjs @@ -0,0 +1,125 @@ +// @ts-check +import { defineConfig } from 'astro/config'; +import starlight from '@astrojs/starlight'; +import react from '@astrojs/react'; +import mdx from '@astrojs/mdx'; +import tailwindcss from '@tailwindcss/vite'; +import starlightSidebarTopics from 'starlight-sidebar-topics'; +import rehypeCodeGroup from 'rehype-code-group'; +import expressiveCode from 'astro-expressive-code'; +import { pluginFullscreen } from 'expressive-code-fullscreen'; +import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'; + +// https://astro.build/config +export default defineConfig({ + integrations: [ + starlight({ + title: 'React Admin Headless', + customCss: ['./src/styles/global.css'], + social: [ + { + icon: 'github', + label: 'GitHub', + href: 'https://github.com/withastro/starlight', + }, + ], + plugins: [ + starlightSidebarTopics([ + { + label: 'Shadcn UI', + link: '/shadcn/guides/architecture', + icon: 'open-book', + items: [ + { + label: 'Guides & Concepts', + items: [ + // Each item here is one entry in the navigation menu. + { + label: 'General Concepts', + slug: 'shadcn/guides/architecture', + }, + { + label: 'Data Fetching', + slug: 'shadcn/guides/datafetchingguide', + }, + { + label: 'CRUD pages', + slug: 'shadcn/guides/crud', + attrs: { + class: 'flex items-center', + }, + badge: { + text: '', + variant: 'default', + class: 'ee-badge', + }, + }, + ], + }, + { + label: 'Reference', + autogenerate: { + directory: 'shadcn/reference', + }, + }, + ], + }, + { + label: 'Headless', + link: '/headless/guides/architecture', + icon: 'open-book', + items: [ + { + label: 'Guides & Concepts', + items: [ + // Each item here is one entry in the navigation menu. + { + label: 'General Concepts', + slug: 'headless/guides/architecture', + }, + { + label: 'Data Fetching', + slug: 'headless/guides/datafetchingguide', + }, + { + label: 'CRUD pages', + slug: 'headless/guides/crud', + attrs: { + class: 'flex items-center', + }, + badge: { + text: '', + variant: 'default', + class: 'ee-badge', + }, + }, + ], + }, + { + label: 'Reference', + autogenerate: { + directory: 'headless/reference', + }, + }, + ], + }, + ]), + ], + components: { + // Override the default `SocialIcons` component. + Sidebar: './src/components/CustomSidebar.astro', + }, + }), + expressiveCode({ + plugins: [pluginFullscreen(), pluginCollapsibleSections()], + }), + react(), + mdx(), + ], + markdown: { + rehypePlugins: [rehypeCodeGroup], + }, + vite: { + plugins: [tailwindcss()], + }, +}); diff --git a/docs_headless/package.json b/docs_headless/package.json new file mode 100644 index 00000000000..0d11fc07905 --- /dev/null +++ b/docs_headless/package.json @@ -0,0 +1,36 @@ +{ + "name": "ra-core-doc", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/mdx": "^4.3.1", + "@astrojs/react": "^4.3.0", + "@astrojs/starlight": "0.35.2", + "@astrojs/starlight-tailwind": "^4.0.1", + "@expressive-code/plugin-collapsible-sections": "^0.41.3", + "@tailwindcss/vite": "^4.1.11", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "astro": "5.12.6", + "astro-expressive-code": "^0.41.3", + "expressive-code-fullscreen": "^1.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "rehype-code-group": "^0.2.4", + "sharp": "^0.34.2", + "starlight-package-managers": "^0.11.0", + "starlight-sidebar-topics": "^0.6.0", + "tailwindcss": "^4.1.11" + }, + "devDependencies": { + "prettier": "^3.2.5", + "prettier-plugin-astro": "^0.14.1" + } +} diff --git a/docs_headless/public/favicon.svg b/docs_headless/public/favicon.svg new file mode 100644 index 00000000000..cba5ac140a2 --- /dev/null +++ b/docs_headless/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs_headless/public/img/premium.svg b/docs_headless/public/img/premium.svg new file mode 100644 index 00000000000..324bd67bcba --- /dev/null +++ b/docs_headless/public/img/premium.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs_headless/src/assets/SPA-lifecycle.png b/docs_headless/src/assets/SPA-lifecycle.png new file mode 100644 index 00000000000..fe6b89f070b Binary files /dev/null and b/docs_headless/src/assets/SPA-lifecycle.png differ diff --git a/docs_headless/src/assets/data-provider.png b/docs_headless/src/assets/data-provider.png new file mode 100644 index 00000000000..3fd7b4d2c24 Binary files /dev/null and b/docs_headless/src/assets/data-provider.png differ diff --git a/docs_headless/src/assets/houston.webp b/docs_headless/src/assets/houston.webp new file mode 100644 index 00000000000..930c164974a Binary files /dev/null and b/docs_headless/src/assets/houston.webp differ diff --git a/docs_headless/src/assets/providers.png b/docs_headless/src/assets/providers.png new file mode 100644 index 00000000000..d8fb881cc5e Binary files /dev/null and b/docs_headless/src/assets/providers.png differ diff --git a/docs_headless/src/assets/tutorial_guessed_list.png b/docs_headless/src/assets/tutorial_guessed_list.png new file mode 100644 index 00000000000..86ecda525a6 Binary files /dev/null and b/docs_headless/src/assets/tutorial_guessed_list.png differ diff --git a/docs_headless/src/components/CustomSidebar.astro b/docs_headless/src/components/CustomSidebar.astro new file mode 100644 index 00000000000..6c7981ef7ab --- /dev/null +++ b/docs_headless/src/components/CustomSidebar.astro @@ -0,0 +1,34 @@ +--- +import MobileMenuFooter from '@astrojs/starlight/components/MobileMenuFooter.astro'; +import SidebarPersister from '@astrojs/starlight/components/SidebarPersister.astro'; +import SidebarSublist from '@astrojs/starlight/components/SidebarSublist.astro'; +import StarlightSidebarTopicsSidebar from 'starlight-sidebar-topics/components/Sidebar.astro'; + +const { sidebar } = Astro.locals.starlightRoute; +--- + + + + + + + + + +
+ +
diff --git a/docs_headless/src/content.config.ts b/docs_headless/src/content.config.ts new file mode 100644 index 00000000000..d68e7959951 --- /dev/null +++ b/docs_headless/src/content.config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), +}; diff --git a/docs_headless/src/content/docs/headless/guides/Architecture.md b/docs_headless/src/content/docs/headless/guides/Architecture.md new file mode 100644 index 00000000000..673a2162aa8 --- /dev/null +++ b/docs_headless/src/content/docs/headless/guides/Architecture.md @@ -0,0 +1,282 @@ +--- +title: "Key Concepts" +--- + +React-admin relies on a several design decisions that structure its codebase. + +## Single-Page Application + +React-admin is specifically designed to build [Single-Page Applications (SPA)](https://en.wikipedia.org/wiki/Single-page_application). In a react-admin app, the browser fetches the required HTML, CSS, and JavaScript to render the application only once. Subsequently, data is fetched from APIs through AJAX calls. This is in contrast to traditional web applications, where the browser fetches a new HTML page for each screen. + +![SPA lifecycle](../../../../assets/SPA-lifecycle.png) + +The SPA architecture ensures that react-admin apps are [exceptionally fast](./Features.md#fast), easy to host, and compatible with existing APIs without requiring a dedicated backend. + +To achieve this, react-admin utilizes an internal router, powered by `react-router`, to display the appropriate screen when the user clicks on a link. Developers can define routes using the [``](./Resource.md) component for CRUD routes and the [``](./CustomRoutes.md) component for other routes. + +For example, the following react-admin application: + +```jsx +import { CoreAdminContext, CoreAdminUI, Resource, CustomRoutes } from 'ra-core'; +import { Route } from 'react-router-dom'; + +export const App = () => ( + + + + + + } /> + } /> + + + } /> + } /> + + + +); +``` + +Declares the following routes: + +- `/labels`: `` +- `/labels/:id`: `` +- `/labels/:id/show`: `` +- `/genres`: `` +- `/artists`: `` +- `/artists/:id`: `` +- `/artists/create`: `` +- `/artists/:id/songs`: `` +- `/artists/:id/songs/:songId`: `` +- `/profile`: `` +- `/organization`: `` + +The `` component allows react-admin to automatically link CRUD pages between them, including those for related entities. This approach allows you to think about your application in terms of entities, rather than getting bogged down by managing routes. + +## Providers + +React-admin does not make any assumptions about the specific structure of your API. Instead, it defines its own syntax for data fetching, authentication, internationalization, and preferences. To interact with your API, react-admin relies on adapters called **providers**. + +![Providers](../../../../assets/providers.png) + +For example, to fetch a list of records from the API, you would use the `dataProvider` object as follows: + +```jsx +dataProvider.getList('posts', { + pagination: { page: 1, perPage: 5 }, + sort: { field: 'title', order: 'ASC' }, + filter: { author_id: 12 }, +}).then(response => { + console.log(response); +}); +// { +// data: [ +// { id: 452, title: "Harry Potter Cast: Where Now?", author_id: 12 }, +// { id: 384, title: "Hermione: A Feminist Icon", author_id: 12 }, +// { id: 496, title: "Marauder's Map Mysteries", author_id: 12 }, +// { id: 123, title: "Real-World Roots of Wizard Spells", author_id: 12 }, +// { id: 189, title: "Your True Hogwarts House Quiz", author_id: 12 }, +// ], +// total: 27 +// } +``` + +The `dataProvider.getList()` method is responsible for translating this request into the appropriate HTTP request to your API. When using the REST data provider, the above code will translate to: + +``` +GET http://path.to.my.api/posts?sort=["title","ASC"]&range=[0, 4]&filter={"author_id":12} + +HTTP/1.1 200 OK +Content-Type: application/json +Content-Range: posts 0-4/27 +[ + { id: 452, title: "Harry Potter Cast: Where Now?", author_id: 12 }, + { id: 384, title: "Hermione: A Feminist Icon", author_id: 12 }, + { id: 496, title: "Marauder's Map Mysteries", author_id: 12 }, + { id: 123, title: "Real-World Roots of Wizard Spells", author_id: 12 }, + { id: 189, title: "Your True Hogwarts House Quiz", author_id: 12 }, +] +``` + +React-admin comes with [more than 50 data providers](./DataProviderList.md) for various backends, including REST, GraphQL, Firebase, Django REST Framework, API Platform, and more. If these providers do not suit your API, you have the flexibility to [develop a custom provider](./DataProviderWriting.md). + +This approach is why react-admin components do not call `fetch` or `axios` directly. Instead, they rely on the data provider to fetch data from the API. Similarly, it is recommended that your custom components follow the same pattern and utilize [data provider hooks](./Actions.md), such as [`useGetList`](./useGetList.md): + +```jsx +import { useGetList } from 'ra-core'; + +const MyComponent = () => { + const { data, total, loading, error } = useGetList('posts', { + pagination: { page: 1, perPage: 5 }, + sort: { field: 'title', order: 'ASC' }, + filter: { author_id: 12 }, + }); + + if (loading) return ; + if (error) return ; + return ( +
+

Found {total} posts matching your query

+
    + {data.map(record => ( +
  • {record.title}
  • + ))} +
+
+ ) +}; +``` + +By using `useGetList`, you gain various benefits beyond a simple `fetch`: it handles user credentials, triggers loading indicators, manages loading states, handles errors, caches results for future use, and controls the data shape, among other things. + +Whenever you need to communicate with a server, you will use these providers. Since they are specialized for their respective domains and tightly integrated with react-admin, they will save you a significant amount of time and effort. + +## Composition + +React-admin avoids components that accept an overwhelming number of props, which are often referred to as "God Components." Instead, react-admin encourages the use of composition, where components accept subcomponents (either through children or specific props) to handle a share of the logic. + +This approach enables you to override specific parts of the logic of a component by composing it with another component. + +For instance, the `` component accepts an `accessDenied` prop that allows you to define what to render when users don't have the required permissions: + +```jsx +import { CanAccess } from 'ra-core'; +import { PremiumStatsFeature } from './PremiumStatsFeature'; +import { UpgradeToPremium } from './UpgradeToPremium'; + +export const Dashboard = ({ children }) => ( +
+ }> + + +
+); +``` + +## Hooks + +`ra-core` consists of hooks and headless components that hide the framework's implementation details, allowing you to focus on your business logic. + +For example, the `useDeleteWithConfirmController` button used in `pessimistic` mode renders a confirmation dialog when clicked and then calls the `dataProvider.delete()` method for the current record. If you want the same feature but with a different UI, you can use the `useDeleteWithConfirmController` hook: + +```jsx +import { useDeleteWithConfirmController, useRecordContext, useResourceContext } from 'ra-core'; +import { Button, Confirm } from 'my-awesome-ui-library'; + +const DeleteButton = () => { + const resource = useResourceContext(); + const record = useRecordContext(); + const { + open, + isPending, + handleDialogOpen, + handleDialogClose, + handleDelete, + } = useDeleteWithConfirmController({ redirect: 'list' }); + + return ( + + + + + ); +}; +``` + +The fact that hook names often end with `Controller` is intentional and reflects the use of [the Model-View-Controller (MVC) pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) for complex components in react-admin. + +- The Controller logic is handled by React hooks (e.g. `useDeleteWithConfirmController`). +- The view logic is managed by React components (e.g. ``). +- The model logic is left to the developer, and react-admin simply defines the interface that the model must expose through its Providers. + +React-admin exposes [dozens of hooks](./Reference.md#hooks) to assist you in building your own components. You can construct an entire react-admin application by using one of our UI packages such as `ra-ui-materialui` for Material UI components or [shadcn-admin-kit](https://github.com/marmelab/shadcn-admin-kit) for Shadcn UI or build your own UI layer. This flexibility allows you to tailor the application to your specific needs and preferences. + +## Context: Pull, Don't Push + +Communication between components can be challenging, especially in large React applications, where passing props down several levels can become cumbersome. React-admin addresses this issue using a pull model, where components expose props to their descendants via a context, and descendants can consume these props using custom hooks. + +Whenever a react-admin component fetches data or defines a callback, it creates a context and places the data and callback in it. + +For instance, the `` component creates an `I18NProviderContext`, which exposes the `translate` function. All components in the application can use the `useTranslate` hook or the `` component, which reads the `I18NProviderContext`, for translating labels and messages. + +```jsx +import { Translate, useTranslate } from 'ra-core'; + +export const MyHelloButton = () => { + const translate = useTranslate(); + const handleClick = () => { + alert(translate('root.button.hello_world.message')) + } + return ( + + ); +}; +``` + +Similarly, the `` component fetches a record and exposes it via a `RecordContext`. Inside the `` component, you can use the `useRecordContext` hook to access the record data. For example, you can use it to display a map of the record's location. + +```jsx +import { ShowBase, useRecordContext } from 'ra-core'; +import { MapContainer, TileLayer, Marker } from 'react-leaflet'; + +const LocationField = ({ source }) => { + const record = useRecordContext(props); // use the RecordContext created by + if (!record) return null; + + return ( + + + + + ); +}; + +const StoreShowPage = () => ( + {/* create a RecordContext */} +
+ +
+
+) +``` + +This approach eliminates the need for a dependency injection system and provides an elegant solution to access data and callbacks from higher levels in the render tree. + +So when you write a component that needs to access data or callbacks defined higher in the render tree, you can always find a context to get it. + +Contexts are fundamental concepts in React Admin. If you are not familiar with them, don't hesitate to read the [React documentation on Context](https://react.dev/learn/passing-data-deeply-with-context). Understanding contexts will greatly enhance your understanding of how react-admin leverages them to create a powerful and flexible framework. + +## 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. + +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/) and [react-router](https://reacttraining.com/react-router/). It's a **framework** made to speed up and facilitate the development of single-page apps in React. \ No newline at end of file diff --git a/docs_headless/src/content/docs/headless/guides/CRUD.md b/docs_headless/src/content/docs/headless/guides/CRUD.md new file mode 100644 index 00000000000..95eee11f546 --- /dev/null +++ b/docs_headless/src/content/docs/headless/guides/CRUD.md @@ -0,0 +1,469 @@ +--- +title: "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. + + + +## Page Components + +React-admin provides page components for CRUD operations: + +- [``](./ListTutorial.md) displays a list of records +- [``](./Show.md) displays a record in read-only mode +- [``](./EditTutorial.md) displays a form to edit a record +- [``](./Create.md) displays a form to create a record + +Each component 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 `` component: + +```jsx +import { List, DataTable } from 'react-admin'; + +const PostList = () => ( + + + + + + + +); +``` + +Here, the `` component will call `dataProvider.getList('posts')` to fetch the list of posts and create a `ListContext` to store the data. The `` 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 `` component. + +## Page Context + +`` and other page components don't just fetch data; they provide a 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 ``'s children components like `` 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; +``` + +## CRUD Routes + +You could declare the CRUD routes manually using react-router's `` component. But it's such a typical pattern that react-admin provides a shortcut: the [``](./Resource.md) component. + +```jsx + +``` + +This is the equivalent of the following react-router configuration: + +```jsx + + + } /> + } /> + } /> + } /> + + +``` + +`` defines a `ResourceContext` storing the current resource `name`. This context is used by the ``, ``, ``, and `` components to determine the resource they should fetch. So when declaring page components with ``, you don't need to pass the `resource` prop to them. + +```diff +import { List, DataTable } from 'react-admin'; + +const PostList = () => ( +- ++ + + + + + + +); +``` + +Check [the `` documentation](./Resource.md) to learn more about routing and resource context. + +## The List Page + +To build list pages, developers primarily use the [``](./List.md) component. It fetches a list of records from the data provider and delegates the rendering to its child component (often a [``](./DataTable.md), as in the example below). + +```jsx +import { List, DataTable, TextInput} from 'react-admin'; + +const filters = []; + +const BookList = () => ( + + + + + + + + +); +``` + +`` also lets you customize the UI for filters and pagination. As for ``, it provides tons of customization options, like row expanders, bulk actions, and column chooser. You can learn more in the [List Tutorial](./ListTutorial.md). + +### List Layouts + +You can use any of the following components to build the list page: + + + + + + + + + + +
+ + + + <DataTable> + + + + + <DatagridAG> +
+ + + + <SimpleList> + + + + + <Calendar> +
+ +Inside these list layouts, you can use any of react-admin’s Field components to display the record data. + +### Filter Components + + + + + + + + + + +
+ + + + Filter Button/Form Combo + + + + + <FilterList> Sidebar +
+ + + + <StackedFilters> Dialog + + + + + Global <Search> +
+ +### Buttons + +- [``](./Buttons.md#editbutton): Go to the edit page for a record +- [``](./EditInDialogButton.md): Edit a record in a dialog +- [``](./Buttons.md#exportbutton): A button to export the list data +- [``](./Buttons.md#createbutton): A button to create a new record +- [``](./SortButton.md): A button to sort the list +- [``](./SelectColumnsButton.md): A button to select the columns to display in a DataTable +- [``](./Buttons.md#bulkupdatebutton): A button to update selected records +- [``](./Buttons.md#bulkdeletebutton): A button to delete selected records +- [``](./List.md#actions): A toolbar with a create and an export button + +### Misc + +- [``](./Pagination.md): Renders the page count and buttons to navigate to the previous and next pages +- [``](./SavedQueriesList.md): Lets user save a combination of filters + +### Alternatives to List + +You can also use specialized alternatives to the `` component, which offer type-specific features: + +- [``](./TreeWithDetails.md): A tree view with a detail view for each node +- [``](./Calendar.md#completecalendar): A calendar view for events +- [``](./InfiniteList.md): A list with infinite scrolling + +## The Show Page + +Show pages rely on the [``](./Show.md) component. It reads the record id from the URL, fetches the record from the data provider, and delegates the rendering to its child component, e.g., a [``](./SimpleShowLayout.md): + +```jsx +import { Show, SimpleShowLayout, TextField} from 'react-admin'; + +const BookShow = () => ( + + + + + + + + +); +``` + +You can learn more in the [Show Tutorial](./ShowTutorial.md). + +### Show Layouts + +You can use any of the following components to build the show page: + + + + + + +
+ + + + <SimpleShowLayout> + + + + + <TabbedShowLayout> +
+ +Inside these show layouts, you can use any of react-admin's [Field components](./Fields.md) to display the record data. + +### Buttons + +- [``](./Buttons.md#editbutton): Go to the edit page for a record +- [``](./Buttons.md#createbutton): A button to create a new record +- [``](./Buttons.md#listbutton): Go to the list page for a record +- [``](./Buttons.md#clonebutton): Clone a record + +### Misc + +- [``](./Labeled.md): A component to display a field with a label + +## The Edit & Create Pages + +The [``](./Edit.md) and [``](./Create.md) components are very similar. They are useful for editing a record - either fetched from the API or a new record. Typically, the child component of these pages is a form layout, like a [``](./SimpleForm.md): + +```jsx +import { Edit, SimpleForm, TextInput} from 'react-admin'; + +const BookEdit = () => ( + + + + + + + + +); +``` + +You can learn more in the [Edit Tutorial](./EditTutorial.md). + +### Form Layouts + +You can use any of the following components to build the edit and create pages: + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Forms are very powerful in react-admin. They support default values, sync and async validation, dependent inputs, access control, sub-forms, and more. Check out the [Forms introduction](./Form.md) for more information. Inside the form layouts, you can use any of react-admin's [Input components](./Inputs.md) to edit the record data. + +### Buttons + +- [``](./Buttons.md#showbutton): Go to the show page for a record +- [``](./SaveButton.md): The submit button for the form +- [``](./Buttons.md#clonebutton): Clone a record + +### Misc + +- [``](./Toolbar.md): Customize the form toolbar +- [``](./JsonSchemaForm.md): A form based on a JSON schema +- [``](./AutoSave.md): Automatically save the form + +## 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 = () => ( + + + +); +``` + +Guesser components start by fetching data from the API, analyzing the shape of the response, and 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](../../../../assets/tutorial_guessed_list.png) + +Check the following components to learn more about guessers: + +- [``](./ListGuesser.md) +- [``](./EditGuesser.md) +- [``](./ShowGuesser.md) + +## Headless Variants + +`` and other page components render their children (e.g., ``) 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**: ``, ``, ``, `` + +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 ( +
+

Posts

+
    + {ids.map(id => ( +
  • {data[id].title}
  • + ))} +
+

Total: {total}

+
+ ); +}; +``` + +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, DataTable } from 'react-admin'; + +const MyList = () => ( + + + + + + + +); +``` diff --git a/docs_headless/src/content/docs/headless/guides/datafetchingguide.mdx b/docs_headless/src/content/docs/headless/guides/datafetchingguide.mdx new file mode 100644 index 00000000000..504ffb89a2b --- /dev/null +++ b/docs_headless/src/content/docs/headless/guides/datafetchingguide.mdx @@ -0,0 +1,420 @@ +--- +title: "Data Fetching" +--- +import { SuccessCase } from '../../../../../../packages/ra-core/src/dataProvider/useUpdate.optimistic.stories.tsx' + +You can build a react-admin app on top of any API, whether it uses REST, GraphQL, RPC, or even SOAP, regardless of the dialect it uses. This works because react-admin doesn't use `fetch` directly. Instead, it uses a Data Provider object to interface with your API and [React Query](https://tanstack.com/query/v5/docs/react/overview) to handle data fetching. + +## The Data Provider + +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`. + +![Backend agnostic](../../../../assets/data-provider.png) + +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 +const response = await dataProvider.getOne('posts', { id: 123 }); +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. + +A Data Provider must implement the following methods: + +```jsx +const dataProvider = { + async getList(resource, { sort, filter, pagination }) => ({ data: Record[], total: number }), + async getOne(resource, { id }) => ({ data: Record }), + async getMany(resource, { ids }) => ({ data: Record[] }), + async getManyReference(resource, { target, id, sort, filter, pagination }) => ({ data: Record[], total: number }), + async create(resource, { data }) => ({ data: Record }), + async update(resource, { id, data }) => ({ data: Record }), + async updateMany(resource, { ids, data }) => ({ data: Identifier[] }), + async delete(resource, { id } ) => ({ data: Record }), + async deleteMany(resource, { ids }) => ({ data: Identifier[] }), +} +``` + +**Tip**: A Data Provider can have [additional methods](./Actions.md#calling-custom-methods) beyond these 9. For example, you can add custom methods for non-REST API endpoints, tree structure manipulations, or realtime updates. + +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 JavaScript. It doesn't care if your API is a third-party API or a homegrown 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 `` and ``, reference components like `` and ``, action Buttons like `` and ``, and many more. + +If you need to call the Data Provider directly from your components, you can use the specialized hooks provided by react-admin: + +* [`useGetList`](./useGetList.md) +* [`useGetOne`](./useGetOne.md) +* [`useGetMany`](./useGetMany.md) +* [`useGetManyReference`](./useGetManyReference.md) +* [`useCreate`](./useCreate.md) +* [`useUpdate`](./useUpdate.md) +* [`useUpdateMany`](./useUpdateMany.md) +* [`useDelete`](./useDelete.md) +* [`useDeleteMany`](./useDeleteMany.md) + +For instance, to call `dataProvider.getOne()`, use the `useGetOne` hook: + +```jsx +import { useGetOne } from 'react-admin'; +import { Loading, Error } from './MyComponents'; + +const UserProfile = ({ userId }) => { + const { data: user, isPending, error } = useGetOne('users', { id: userId }); + + if (isPending) return ; + if (error) return ; + if (!user) return null; + + return ( +
    +
  • Name: {user.name}
  • +
  • Email: {user.email}
  • +
+ ) +}; +``` + +You can also call the `useDataProvider` hook to access the `dataProvider` directly: + +```jsx +import { useDataProvider } from 'react-admin'; + +const BanUserButton = ({ userId }) => { + const dataProvider = useDataProvider(); + const handleClick = () => { + dataProvider.update('users', { id: userId, data: { isBanned: true } }); + }; + return + +); +``` + +This approach enables you to override specific parts of the logic of a component by composing it with another component. + +The trade-off with this approach is that sometimes react-admin may require you to override several components just to enable one specific feature. For instance, to override the Menu, you must first create a custom layout using your menu as the `` prop, then pass it as the `` prop: + +```jsx +// in src/MyLayout.js +import { Layout } from 'react-admin'; +import { Menu } from './Menu'; + +export const MyLayout = ({ children }) => ( + + {children} + +); + +// in src/App.js +import { Admin } from 'react-admin'; +import { MyLayout } from './MyLayout'; + +const App = () => ( + + // ... + +); +``` + +Although this drawback exists, we accept it because the use of composition in react-admin makes the components highly extensible, and it significantly improves the readability and maintainability of the code. + +## Hooks + +When you find that you cannot tweak a react-admin component using props, you can always turn to the lower-level API: hooks. In fact, the core of react-admin is a headless library called `ra-core`, which primarily consists of hooks. These hooks hide the framework's implementation details, allowing you to focus on your business logic. It's perfectly normal to use react-admin hooks in your own components if the default UI doesn't meet your specific requirements. + +For example, the `` button used in `pessimistic` mode renders a confirmation dialog when clicked and then calls the `dataProvider.delete()` method for the current record. If you want the same feature but with a different UI, you can use the `useDeleteWithConfirmController` hook: + +```jsx +const DeleteButton = () => { + const resource = useResourceContext(); + const record = useRecordContext(); + const { + open, + isPending, + handleDialogOpen, + handleDialogClose, + handleDelete, + } = useDeleteWithConfirmController({ redirect: 'list' }); + + return ( + + + + + ); +}; +``` + +The fact that hook names often end with `Controller` is intentional and reflects the use of [the Model-View-Controller (MVC) pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) for complex components in react-admin. + +- The Controller logic is handled by React hooks (e.g. `useListController`). +- The view logic is managed by React components (e.g. ``). +- The model logic is left to the developer, and react-admin simply defines the interface that the model must expose through its Providers. + +React-admin exposes [dozens of hooks](./Reference.md#hooks) to assist you in building your own components. You can even construct an entire react-admin application without relying on the Material UI components and use a different UI kit if desired (see for instance [shadcn-admin-kit](https://github.com/marmelab/shadcn-admin-kit), a library for building admin apps with Shadcn UI). This flexibility allows you to tailor the application to your specific needs and preferences. + +## Context: Pull, Don't Push + +Communication between components can be challenging, especially in large React applications, where passing props down several levels can become cumbersome. React-admin addresses this issue using a pull model, where components expose props to their descendants via a context, and descendants can consume these props using custom hooks. + +Whenever a react-admin component fetches data or defines a callback, it creates a context and places the data and callback in it. + +For instance, the `` component creates an `I18NProviderContext`, which exposes the `translate` function. All components in the application can utilize the `useTranslate` hook, which reads the `I18NProviderContext`, for translating labels and messages. + +```jsx +import { useTranslate } from 'ra-core'; + +export const MyHelloButton = ({ handleClick }) => { + const translate = useTranslate(); + return ( + + ); +}; +``` + +Similarly, the `` component fetches a record and exposes it via a `RecordContext`. Inside the `` component, you can use the `useRecordContext` hook to access the record data. For example, you can use it to display a map of the record's location. + +```jsx +import { ShowBase, useRecordContext } from 'ra-core'; +import { MapContainer, TileLayer, Marker } from 'react-leaflet'; + +const LocationField = ({ source }) => { + const record = useRecordContext(props); // use the RecordContext created by + if (!record) return null; + + return ( + + + + + ); +}; + +const StoreShowPage = () => ( + {/* create a RecordContext */} +
+ +
+
+) +``` + +This approach eliminates the need for a dependency injection system and provides an elegant solution to access data and callbacks from higher levels in the render tree. + +So when you write a component that needs to access data or callbacks defined higher in the render tree, you can always find a context to get it. + +Contexts are fundamental concepts in React Admin. If you are not familiar with them, don't hesitate to read the [React documentation on Context](https://react.dev/learn/passing-data-deeply-with-context). Understanding contexts will greatly enhance your understanding of how react-admin leverages them to create a powerful and flexible framework. + +## 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. + +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. \ No newline at end of file diff --git a/docs_headless/src/content/docs/shadcn/guides/CRUD.md b/docs_headless/src/content/docs/shadcn/guides/CRUD.md new file mode 100644 index 00000000000..95eee11f546 --- /dev/null +++ b/docs_headless/src/content/docs/shadcn/guides/CRUD.md @@ -0,0 +1,469 @@ +--- +title: "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. + + + +## Page Components + +React-admin provides page components for CRUD operations: + +- [``](./ListTutorial.md) displays a list of records +- [``](./Show.md) displays a record in read-only mode +- [``](./EditTutorial.md) displays a form to edit a record +- [``](./Create.md) displays a form to create a record + +Each component 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 `` component: + +```jsx +import { List, DataTable } from 'react-admin'; + +const PostList = () => ( + + + + + + + +); +``` + +Here, the `` component will call `dataProvider.getList('posts')` to fetch the list of posts and create a `ListContext` to store the data. The `` 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 `` component. + +## Page Context + +`` and other page components don't just fetch data; they provide a 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 ``'s children components like `` 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; +``` + +## CRUD Routes + +You could declare the CRUD routes manually using react-router's `` component. But it's such a typical pattern that react-admin provides a shortcut: the [``](./Resource.md) component. + +```jsx + +``` + +This is the equivalent of the following react-router configuration: + +```jsx + + + } /> + } /> + } /> + } /> + + +``` + +`` defines a `ResourceContext` storing the current resource `name`. This context is used by the ``, ``, ``, and `` components to determine the resource they should fetch. So when declaring page components with ``, you don't need to pass the `resource` prop to them. + +```diff +import { List, DataTable } from 'react-admin'; + +const PostList = () => ( +- ++ + + + + + + +); +``` + +Check [the `` documentation](./Resource.md) to learn more about routing and resource context. + +## The List Page + +To build list pages, developers primarily use the [``](./List.md) component. It fetches a list of records from the data provider and delegates the rendering to its child component (often a [``](./DataTable.md), as in the example below). + +```jsx +import { List, DataTable, TextInput} from 'react-admin'; + +const filters = []; + +const BookList = () => ( + + + + + + + + +); +``` + +`` also lets you customize the UI for filters and pagination. As for ``, it provides tons of customization options, like row expanders, bulk actions, and column chooser. You can learn more in the [List Tutorial](./ListTutorial.md). + +### List Layouts + +You can use any of the following components to build the list page: + + + + + + + + + + +
+ + + + <DataTable> + + + + + <DatagridAG> +
+ + + + <SimpleList> + + + + + <Calendar> +
+ +Inside these list layouts, you can use any of react-admin’s Field components to display the record data. + +### Filter Components + + + + + + + + + + +
+ + + + Filter Button/Form Combo + + + + + <FilterList> Sidebar +
+ + + + <StackedFilters> Dialog + + + + + Global <Search> +
+ +### Buttons + +- [``](./Buttons.md#editbutton): Go to the edit page for a record +- [``](./EditInDialogButton.md): Edit a record in a dialog +- [``](./Buttons.md#exportbutton): A button to export the list data +- [``](./Buttons.md#createbutton): A button to create a new record +- [``](./SortButton.md): A button to sort the list +- [``](./SelectColumnsButton.md): A button to select the columns to display in a DataTable +- [``](./Buttons.md#bulkupdatebutton): A button to update selected records +- [``](./Buttons.md#bulkdeletebutton): A button to delete selected records +- [``](./List.md#actions): A toolbar with a create and an export button + +### Misc + +- [``](./Pagination.md): Renders the page count and buttons to navigate to the previous and next pages +- [``](./SavedQueriesList.md): Lets user save a combination of filters + +### Alternatives to List + +You can also use specialized alternatives to the `` component, which offer type-specific features: + +- [``](./TreeWithDetails.md): A tree view with a detail view for each node +- [``](./Calendar.md#completecalendar): A calendar view for events +- [``](./InfiniteList.md): A list with infinite scrolling + +## The Show Page + +Show pages rely on the [``](./Show.md) component. It reads the record id from the URL, fetches the record from the data provider, and delegates the rendering to its child component, e.g., a [``](./SimpleShowLayout.md): + +```jsx +import { Show, SimpleShowLayout, TextField} from 'react-admin'; + +const BookShow = () => ( + + + + + + + + +); +``` + +You can learn more in the [Show Tutorial](./ShowTutorial.md). + +### Show Layouts + +You can use any of the following components to build the show page: + + + + + + +
+ + + + <SimpleShowLayout> + + + + + <TabbedShowLayout> +
+ +Inside these show layouts, you can use any of react-admin's [Field components](./Fields.md) to display the record data. + +### Buttons + +- [``](./Buttons.md#editbutton): Go to the edit page for a record +- [``](./Buttons.md#createbutton): A button to create a new record +- [``](./Buttons.md#listbutton): Go to the list page for a record +- [``](./Buttons.md#clonebutton): Clone a record + +### Misc + +- [``](./Labeled.md): A component to display a field with a label + +## The Edit & Create Pages + +The [``](./Edit.md) and [``](./Create.md) components are very similar. They are useful for editing a record - either fetched from the API or a new record. Typically, the child component of these pages is a form layout, like a [``](./SimpleForm.md): + +```jsx +import { Edit, SimpleForm, TextInput} from 'react-admin'; + +const BookEdit = () => ( + + + + + + + + +); +``` + +You can learn more in the [Edit Tutorial](./EditTutorial.md). + +### Form Layouts + +You can use any of the following components to build the edit and create pages: + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +Forms are very powerful in react-admin. They support default values, sync and async validation, dependent inputs, access control, sub-forms, and more. Check out the [Forms introduction](./Form.md) for more information. Inside the form layouts, you can use any of react-admin's [Input components](./Inputs.md) to edit the record data. + +### Buttons + +- [``](./Buttons.md#showbutton): Go to the show page for a record +- [``](./SaveButton.md): The submit button for the form +- [``](./Buttons.md#clonebutton): Clone a record + +### Misc + +- [``](./Toolbar.md): Customize the form toolbar +- [``](./JsonSchemaForm.md): A form based on a JSON schema +- [``](./AutoSave.md): Automatically save the form + +## 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 = () => ( + + + +); +``` + +Guesser components start by fetching data from the API, analyzing the shape of the response, and 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](../../../../assets/tutorial_guessed_list.png) + +Check the following components to learn more about guessers: + +- [``](./ListGuesser.md) +- [``](./EditGuesser.md) +- [``](./ShowGuesser.md) + +## Headless Variants + +`` and other page components render their children (e.g., ``) 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**: ``, ``, ``, `` + +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 ( +
+

Posts

+
    + {ids.map(id => ( +
  • {data[id].title}
  • + ))} +
+

Total: {total}

+
+ ); +}; +``` + +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, DataTable } from 'react-admin'; + +const MyList = () => ( + + + + + + + +); +``` diff --git a/docs_headless/src/content/docs/shadcn/guides/DataFetchingGuide.md b/docs_headless/src/content/docs/shadcn/guides/DataFetchingGuide.md new file mode 100644 index 00000000000..85912c6aa48 --- /dev/null +++ b/docs_headless/src/content/docs/shadcn/guides/DataFetchingGuide.md @@ -0,0 +1,423 @@ +--- +layout: default +title: "Data Fetching" +--- + +You can build a react-admin app on top of any API, whether it uses REST, GraphQL, RPC, or even SOAP, regardless of the dialect it uses. This works because react-admin doesn't use `fetch` directly. Instead, it uses a Data Provider object to interface with your API and [React Query](https://tanstack.com/query/v5/docs/react/overview) to handle data fetching. + +## The Data Provider + +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`. + +![Backend agnostic](../../../../assets/data-provider.png) + +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 +const response = await dataProvider.getOne('posts', { id: 123 }); +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. + +A Data Provider must implement the following methods: + +```jsx +const dataProvider = { + async getList(resource, { sort, filter, pagination }) => ({ data: Record[], total: number }), + async getOne(resource, { id }) => ({ data: Record }), + async getMany(resource, { ids }) => ({ data: Record[] }), + async getManyReference(resource, { target, id, sort, filter, pagination }) => ({ data: Record[], total: number }), + async create(resource, { data }) => ({ data: Record }), + async update(resource, { id, data }) => ({ data: Record }), + async updateMany(resource, { ids, data }) => ({ data: Identifier[] }), + async delete(resource, { id } ) => ({ data: Record }), + async deleteMany(resource, { ids }) => ({ data: Identifier[] }), +} +``` + +**Tip**: A Data Provider can have [additional methods](./Actions.md#calling-custom-methods) beyond these 9. For example, you can add custom methods for non-REST API endpoints, tree structure manipulations, or realtime updates. + +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 JavaScript. It doesn't care if your API is a third-party API or a homegrown 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 `` and ``, reference components like `` and ``, action Buttons like `` and ``, and many more. + +If you need to call the Data Provider directly from your components, you can use the specialized hooks provided by react-admin: + +* [`useGetList`](./useGetList.md) +* [`useGetOne`](./useGetOne.md) +* [`useGetMany`](./useGetMany.md) +* [`useGetManyReference`](./useGetManyReference.md) +* [`useCreate`](./useCreate.md) +* [`useUpdate`](./useUpdate.md) +* [`useUpdateMany`](./useUpdateMany.md) +* [`useDelete`](./useDelete.md) +* [`useDeleteMany`](./useDeleteMany.md) + +For instance, to call `dataProvider.getOne()`, use the `useGetOne` hook: + +```jsx +import { useGetOne } from 'react-admin'; +import { Loading, Error } from './MyComponents'; + +const UserProfile = ({ userId }) => { + const { data: user, isPending, error } = useGetOne('users', { id: userId }); + + if (isPending) return ; + if (error) return ; + if (!user) return null; + + return ( +
    +
  • Name: {user.name}
  • +
  • Email: {user.email}
  • +
+ ) +}; +``` + +You can also call the `useDataProvider` hook to access the `dataProvider` directly: + +```jsx +import { useDataProvider } from 'react-admin'; + +const BanUserButton = ({ userId }) => { + const dataProvider = useDataProvider(); + const handleClick = () => { + dataProvider.update('users', { id: userId, data: { isBanned: true } }); + }; + return