Implementing simple Infinite Scroll component with react-admin
Tags: react, reactadmin, beginners, tutorial
Published: 2023-04-12 15:08:07

## What is react-admin?
React-admin is a UI library built on top of React that allows developers to build CRUD interfaces, focusing on the business logic of applications without worrying about low-level details. It offers an intuitive API, comprehensive documentation, demo apps, and active support, making it a worth-considering choice for creating a single-page admin application.

It relies on a so-called data provider which is an abstraction that is responsible for handling API calls. This Data Provider is then consumed by high-level methods for creating, reading, updating, and deleting data. 


![Data Provider flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i65226c9xjxvv0szj78q.png)


## Introduction
My motivation is to share my knowledge in this field. I believe that the implementation steps of the infinite scroll for react-admin will benefit many beginner users of this library. As this is my first article, I'm also excited to practice and improve my writing skills while contributing to the community.

While there is an existing similar article on [Building a Timeline With React](https://dev.to/fzaninotto/building-a-timeline-with-react-storybook-material-ui-and-react-admin-2cd3) it is now outdated, though the problem-solving approach remains relevant, so it's worth reading IMO. React admin docs demonstrate how to achieve a similar outcome by utilizing their [useInfiniteGetList](https://marmelab.com/react-admin/useInfiniteGetList.html) hook.

In this article, I will demonstrate how to construct a custom \<InfiniteList> component that can replace the standard react-admin \<List> component, enabling reusability "the react-admin way".

This syntax makes react-admin default list component

```javascript
<List>
  <Datagrid>
    <TextField source="id" />
    <TextField source="title" />
  </Datagrid>
</List>
```
and generates the following appearance:
![List component with pagination](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qy2ljgma6hr2fc46s4v1.png)

By implementing our new custom \<InfiniteList> component, the code will look like this:
```javascript
<InfiniteList>
  <Datagrid>
    <TextField source="id" />
    <TextField source="title" />
  </Datagrid>
</InfiniteList>
```
and rendered view:

![List component with infinite scroll](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zuir588eobs295ss1ium.png)


## Setting up the project

A comprehensive tutorial on installing and setting up react admin can be found in the [react-admin docs](https://marmelab.com/react-admin/Tutorial.html). However, I will provide a brief overview here. Alternatively, you can utilize one of the sample applications available in [react-admin repository](https://github.com/marmelab/react-admin/tree/master/examples).

First, create a React application using Vite by running the following command in the desired directory:
`yarn create vite test-admin --template react`
For simplicity, we'll create a JavaScript project. If you prefer TypeScript, replace `react` with `react-ts`. Next, install the react-admin package and the [JSONPlaceholder](https://jsonplaceholder.typicode.com) data provider.
`yarn add react-admin ra-data-json-server`
JSONPlaceholder is the easiest choice to have it up and running fast. It can be replaced by any other data provider if you like. 

Now remove unnecessary code from the `App.jsx` and add blocks required to run the react-admin panel. The results should look like this:
```javascript
import { Admin } from "react-admin";
import jsonServerProvider from "ra-data-json-server";

const dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com");

function App() {
  return <Admin dataProvider={dataProvider} />;
}

export default App
```
To display data from `https://jsonplaceholder.typicode.com`, we need to add a few more components. \<Resource> component is responsible for mapping routes and passing resource name to the context provider. \<List> component handles filter, sort, and pagination logic and adds components for error handling, loading, and title. \<Datagrid> component in short is responsible for iteration over data passed from the \<List>. The final `App.jsx` should appear as follows:

```javascript
import { Admin, Datagrid, List, Resource, TextField } from "react-admin";
import jsonServerProvider from "ra-data-json-server";

const dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com");

function App() {
  return (
    <Admin dataProvider={dataProvider}>
      <Resource name="posts" list={PostList} />
    </Admin>
  )
}

const PostList = () => {
  return (
    <List>
      <Datagrid>
        <TextField source="id" />
        <TextField source="name" />
      </Datagrid>
    </List>
  )
}

export default App
```


## Build component's logic
Now is the time to create our infinite list component. Create a new file named `InfiniteList.jsx`. In our newly created component, we will add react-admin hooks required for our component to work. 
```javascript
export const InfiniteList = ({ children, ...props }) => {
  const resource = useResourceContext();
  const [sort, setSort] = React.useState({ field: "title", order: "ASC" });
  const [selectedIds, { toggle, select, clearSelection }] = 
    useRecordSelection(resource);
  ...
```
the first line will get the resource name from the \<Resource> component. We will need it for handling row selection, fetching correct data, and internal react-admin components. Next line will take care of keeping our sort state. The last one provides selected row IDs and methods required for handling selection.

For fetching data we will use react-admin hook `useInfiniteGetList`. This hook utilizes react-query's hook `useInfiniteQuery` connecting it with the data provider. It is quite similar to the react-admin `useGetList` hook, with a minor difference in the returned data.  The data is contained within the `pages` property. This property is an array of data that can be consumed by our component. Along with the data we also need the following properties `isFetching`, `isLoading`, `refetch` and `hasNextPage`, all of which are returned by this hook. With these additions, our code should look like this:
```javascript
export const InfiniteList = ({ children, ...props }) => {
  const resource = useResourceContext();
  const [sort, setSort] = React.useState({ field: "id", order: "ASC" });
  const [selectedIds, { toggle, select, clearSelection }] =
    useRecordSelection(resource);
  
  const { data, isFetching, isLoading, refetch, hasNextPage, fetchNextPage } =
    useInfiniteGetList(resource, { sort });
```

As said before our data must be flattened to be properly interpreted by the \<Datagrid>. It's also recommended to memoize the data, as not doing so may result in additional re-renders in descendant react-admin components. That's it. This is a minimal logic required for our component to work. The next step is to pass all the properties to their respective components.


## Pass the logic output to the context provider

Just like the \<List> component, our \<InfiniteList> also takes \<Datagrid> as a child. The \<Datagrid> component retrieves the necessary data from the ListContext, so we need to pass our data to the \<ListContextProvider>. Then \<Datagrid> will be able to consume it.

```javascript
  ...
  
  return (
    <ListContextProvider
      value={{
        sort,
        setSort,
        selectedIds,
        onSelect: select,
        onToggleItem: toggle,
        onUnselectItems: clearSelection,
        data: flattenedData,
        isFetching,
        isLoading,
        refetch,
        resource,
        hasNextPage,
      }}
    >
      ...
```
This configuration is sufficient to run the app without any crashes. However, if you used the TypeScript template, you will get an error indicating that some properties are missing.

![ListContextProvider missing properties error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j7ls6zyu7cdllf7wq1o0.png)
Even in plain JS you will be notified that something is wrong (Something, because who would understand what's written in the error description 😅). This is because our simple version does not implement filtering and pagination. To get rid of the error you can create default properties and spread them into the ListContextProvider value prop. Defaults may look like this:

```javascript
const defaultContextListProps = {
  setFilters: () => {},
  setPage: () => {},
  setPerPage: () => {},
  showFilter: () => {},
  hideFilter: () => {},
  displayedFilters: false,
  filterValues: {},
  total: null,
  page: 1,
  perPage: 10,
  hasPreviousPage: false,
};
```

Inside the \<ListContextProvider>, we should place the \<ListView> component to have all the features that the original \<List> component has, such as the title on the app bar, the optional aside component, and the error component. You should also add the `pagination` prop set to `false` in order to hide the pagination component. In the end, it should look like this:


```javascript
  ...
  
  return (
    <ListContextProvider
      value={{
        ...defaultContextListProps
        sort,
        setSort,
        selectedIds,
        onSelect: select,
        onToggleItem: toggle,
        onUnselectItems: clearSelection,
        data: flattenedData,
        isFetching,
        isLoading,
        refetch,
        resource,
        hasNextPage,
      }}
    >
      <ListView {...props} pagination={false}>
        {children}
      </ListView>
    </ListContextProvider>
  )
        
```


## Load more button

Last but not least - The load more button. Without it, our app would not know when to fetch more data. In this example, I will use a simple button that will load more data when clicked. It can be replaced with a component that will use an intersection observer to load more data when it comes into view. We will use the react-admin button and put 3 props to it. The button itself should look like this:
```javascript
<Button
  disabled={!hasNextPage}
  onClick={() => fetchNextPage()}
  label="Load More"
/>
```
Last step is to wrap it in a div and center it to look more pleasant. With this step complete, we are done. Our whole component should look like this:
```javascript
import React from "react";
import {
  Button,
  ListContextProvider,
  ListView,
  useResourceContext,
  useInfiniteGetList,
  useRecordSelection,
} from "react-admin";

const defaultListContextProps = {
  setFilters: () => {},
  setPage: () => {},
  setPerPage: () => {},
  showFilter: () => {},
  hideFilter: () => {},
  displayedFilters: false,
  filterValues: {},
  total: null,
  page: 1,
  perPage: 10,
  hasPreviousPage: false,
};

export const InfiniteList = ({ children, ...props }) => {
  const resource = useResourceContext();
  const [sort, setSort] = React.useState({ field: "id", order: "ASC" });
  const [selectedIds, { toggle, select, clearSelection }] =
    useRecordSelection(resource);

  const { data, isFetching, isLoading, refetch, hasNextPage, fetchNextPage } =
    useInfiniteGetList(resource, { sort });

  const flattenedData = React.useMemo(
    () => data?.pages.map((page) => page.data).flat(),
    [data]
  );

  return (
    <ListContextProvider
      value={{
        ...defaultListContextProps,
        sort,
        setSort,
        selectedIds,
        onSelect: select,
        onToggleItem: toggle,
        onUnselectItems: clearSelection,
        data: flattenedData,
        isFetching,
        isLoading,
        refetch,
        resource,
        hasNextPage,
      }}
    >
      <ListView {...props} pagination={false}>
        {children}
        <div style={{ display: "flex", justifyContent: "center" }}>
          <Button
            disabled={!hasNextPage}
            onClick={() => fetchNextPage()}
            label="Load More"
          />
        </div>
      </ListView>
    </ListContextProvider>
  );
};
```

If you've made it that far. Thank you! If you liked it or not let me know. Negative feedback is also appreciated.😊