DEV Community

yanchesky
yanchesky

Posted on

Implementing simple Infinite Scroll component with react-admin

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

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 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 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

<List>
  <Datagrid>
    <TextField source="id" />
    <TextField source="title" />
  </Datagrid>
</List>
Enter fullscreen mode Exit fullscreen mode

and generates the following appearance:
List component with pagination

By implementing our new custom <InfiniteList> component, the code will look like this:

<InfiniteList>
  <Datagrid>
    <TextField source="id" />
    <TextField source="title" />
  </Datagrid>
</InfiniteList>
Enter fullscreen mode Exit fullscreen mode

and rendered view:

List component with infinite scroll

Setting up the project

A comprehensive tutorial on installing and setting up react admin can be found in the react-admin docs. However, I will provide a brief overview here. Alternatively, you can utilize one of the sample applications available in react-admin repository.

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 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:

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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.

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

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:

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 });
Enter fullscreen mode Exit fullscreen mode

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.

  ...

  return (
    <ListContextProvider
      value={{
        sort,
        setSort,
        selectedIds,
        onSelect: select,
        onToggleItem: toggle,
        onUnselectItems: clearSelection,
        data: flattenedData,
        isFetching,
        isLoading,
        refetch,
        resource,
        hasNextPage,
      }}
    >
      ...
Enter fullscreen mode Exit fullscreen mode

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
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:

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

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:

  ...

  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>
  )

Enter fullscreen mode Exit fullscreen mode

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:

<Button
  disabled={!hasNextPage}
  onClick={() => fetchNextPage()}
  label="Load More"
/>
Enter fullscreen mode Exit fullscreen mode

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)